100 Languages Speedrun: Episode 69: Ioke

So continuing the search for an acceptable Smalltalk, today it's time to check out Ioke, Io-like language for the JVM.

I covered Smalltalk a while back, and Io recently, so you might want to check it out for some extra background. Many of the examples in this episode are adaptations of Io examples, so check it out to compare Io with Ioke.

Hello, World!

This almost works:

#!/usr/bin/env ioke

"Hello, World!" println
$ ./hello.ik
Jan 27, 2022 9:51:01 AM com.google.common.base.internal.Finalizer getInheritableThreadLocalsField
INFO: Couldn't access Thread.inheritableThreadLocals. Reference finalizer threads will inherit thread local values.
Hello, World!

REPL also kinda works, at least until you try to press Ctrl-D:

$ ioke
Jan 27, 2022 9:52:52 AM com.google.common.base.internal.Finalizer getInheritableThreadLocalsField
INFO: Couldn't access Thread.inheritableThreadLocals. Reference finalizer threads will inherit thread local values.
iik> "Hello, World!" println
Hello, World!
+> nil

iik> *** - couldn't find cell 'empty?' on 'false' (Condition Error NoSuchCell)

 data empty?                                      [builtin/iik.ik:47:26]

The following restarts are available:
 0: storeValue           (Store value for: empty?)
 1: useValue             (Use value for: empty?)
 2: abort                (restart: abort)
 3: quit                 (restart: quit)

 dbg:1> 3

Fixing Ioke

It's really disappointing just how many languages fail this basic test:

  • installation with brew install language
  • hello world works with #!, or at least with language hello.language
  • repl works with proper line editing and Ctrl-D when I'm done
  • not printing any extra crap, just the "Hello, World!" I asked for (REPL can print a single line with version number, but no more)

I don't think my expectations are unreasonable. And yes, "compiled" languages can do #! too, most just chose not to.

I tried to search for similar problem, and it has something to do with Java security model, but nobody has a solution. I think something about Java security model changes, and Ioke was never fully updated for the latest JVM. I'll just proceed with Ioke ignoring those two lines of warnings.

Math

Ioke math drops all the Smalltalk silliness, and uses normal operator precedence, and also = for assignment. Comments start with a ; instead of the usual #. I think we'd be in a much happier place if we could all agree on # as a comment character, as this also solves #!, but it's not a big deal in the grand scheme of things.

#!/usr/bin/env ioke

; Assignment symbol is just equals =
a = 2
b = 3
c = 4

; Math follows the usual rules
(a + b * c) println
$ ./math.ik
14

Fibonacci

It's quite close to Io:

#!/usr/bin/env ioke

Number fib = method((self-2) fib + (self-1) fib)
1 fib = method(1)
2 fib = method(1)

for(i <- 1..30, "fib(#{i}) = #{i fib}" println)
$ ./fib.ik
fib(1) = 1
fib(2) = 1
fib(3) = 2
fib(4) = 3
fib(5) = 5
fib(6) = 8
fib(7) = 13
fib(8) = 21
fib(9) = 34
fib(10) = 55
fib(11) = 89
fib(12) = 144
fib(13) = 233
fib(14) = 377
fib(15) = 610
fib(16) = 987
fib(17) = 1597
fib(18) = 2584
fib(19) = 4181
fib(20) = 6765
fib(21) = 10946
fib(22) = 17711
fib(23) = 28657
fib(24) = 46368
fib(25) = 75025
fib(26) = 121393
fib(27) = 196418
fib(28) = 317811
fib(29) = 514229
fib(30) = 832040

Step by step:

  • we define fib method on prototype of Number by Number fib = method(...)
  • the definition is (self-2) fib + (self-1) fib
  • we override that definition on 1 and 2 to return 1 instead
  • looping has much nicer syntax than Io, for(i <- 1..30, ...) - and you can loop over multiple things at once
  • string interpolation is builtin into the language, also big improvement over Io

Also Ioke is really slow. The recursive fib(30) solution takes 0.7s in Python, 6s in Io, 30s in Ioke.

Fizzbuzz

FizzBuzz follows the Io code, with same changes as Fibonacci:

#!/usr/bin/env ioke

Number fizzbuzz = method(
  if(self % 15 == 0, "FizzBuzz",
    if(self % 5 == 0, "Buzz",
      if(self % 3 == 0, "Fizz",
        self))))

for(i <- 1..100, i fizzbuzz println)

Unicode

Like every other JVM-based language, Ioke Unicode support is broken:

#!/usr/bin/env ioke

"Hello" length println
"Żółw" length println
"💩" length println
"Żółw" upper println
"Żółw" lower println
$ ./unicode.ik
5
4
2
ŻÓŁW
żółw

This is really baffling. Ioke is already slow enough that it wouldn't have any measurable additional cost to replace Java's broken string functions by slower implementations that actually work correctly.

Lists

Ioke has nice syntax for defining and printing lists:

#!/usr/bin/env ioke

a = [1, 2, 3, 4, 5]

a map(x, x * 2) println
a select(x, x % 2 == 0) println
a reduce(x, y, x + y) println
a at(0) println
a at(-1) println
a[3] println
$ ./lists.ik
[2, 4, 6, 8, 10]
[2, 4]
15
1
5
4

It supports both Smalltalk style at and atPut operators, as well as mode modern [] and []= syntax.

The funny thing is that Smalltalk started with such crazy syntax, but step by step every new Smalltalk variant is syntactically closer and closer to Ruby. Semantically they keep doing their own unique things.

Dicts

With dicts, Ioke also takes a step towards Ruby:

#!/usr/bin/env ioke

a = {
  "name" => "Alice",
  "last_name" => "Smith",
  "age" => 25,
}
a["age"] = 26

"#{a["name"]} #{a["last_name"]} is #{a["age"]} years old" println
a println
$ ./dicts.ik
Alice Smith is 26 years old
{name=Alice, last_name=Smith, age=26}

Sets

There are also sets, which I mention mostly because Ioke added a small number of Unicode operators. This trend is more and more common in new languages. Nobody wants to go full APL, but for less commonly used things like sets, a few Unicode operators are a nice thing.

#!/usr/bin/env ioke

a = #{1,2,3,1}
b = #{5,4,3,2}

a println
(a ∩ b) println
(a ∪ b) println
(5 ∈ b) println
$ ./sets.ik
[1, 2, 3]
[2, 3]
[1, 2, 3, 4, 5]
true

Weirdly sets are printed like arrays, even though they're a different thing.

Defining new kinds

We can put it in the same file, but let's create this point.ik:

Point = Origin mimic
Point asText = method("<#{x},#{y}>")
Point + = method(other,
  result = Point mimic
  result x = self x + other x
  result y = self y + other y
  result)

Unfortunately unlike Io, Ioke does not have any kind of autoloading, so we need to use() it explicitly. We can then use it with point_math.ik:

#!/usr/bin/env ioke

use("Point")

a = Point mimic
a x = 20
a y = 60

b = Point mimic do(
  x = 400
  y = 9)

(a + b) println
$ ./point_math.ik
<420,69>

Step by step:

  • Io's Object clone is now Origin mimic
  • asText converts to text and it looks quite nice thanks to builtin object interpolation

Better way of defining new kinds

#!/usr/bin/env ioke

Point = Origin mimic do(
  asText = method("<#{x},#{y}>")
  + = method(other, Point with(x: self x + other x, y: self y + other y)))

a = Point with(x: 20, y: 60)
b = Point with(x: 400, y: 9)
(a + b) println
$ ./point2.ik
<420,69>

The with convention from Io is now part of the language. with() creates a new object, then calls appropriate assignments. We don't need to write anything.

Macros

Io had a single method() which evaluated named arguments, and left the rest unevaluated, for macro use.

Ioke distinguishes method() with normal evaluation, from macro() which takes control over evaluation. Here's a maybe macro:

#!/usr/bin/env ioke

random = method(min, max, min + System randomNumber % (max - min + 1))

maybe = macro((random(0,1) == 0) ifTrue(call argAt(0)))

for(i <- 1..10, maybe(i println))
$  ./maybe.ik
1
3
8
9

As far as I can tell, this is exactly as powerful macro system as Io's, but it also allows methods to be more flexible, taking keyword arguments and so on.

Should you use Ioke?

Ioke has seen no development in a decade, and was never anywhere near "production quality", so it's basically an interesting experiment that went nowhere. Obviously you shouldn't use it for anything serious.

I think it's interesting for a few reasons. First, while Lisp-likes have some modern Lisp-style languages like Racket and Clojure, Smalltalk-like line basically fizzled out. So if you want to see how Smalltalks might have evolved, you pretty much need to check such experiments as Self, Io, and Ioke, or really ancient languages like GNU Smalltalk.

By the way the same is true for Forth-likes and APL-likes, both lines fizzled out. Forth-likes at least greatly influenced esoteric languages, for APL-likes I couldn't even find one that would just work out of the box so I could cover it here.

And second, I think they evolved in an interesting way - turning more and more into Ruby syntactically, while going into their own unique semantics. Modern Smalltalk-likes are arguably even more semantically minimalistic than the original, but they are really not interested in syntactic minimalism. And it's not just Smalltalk-likes. You can see similar trend from Erlang to Elixir, from R to Julia, from Java to Kotlin, from C to Crystal, from old JavaScript to ES6+ JavaScript, and so on.

Syntactically, Ruby is insanely influential. Ruby-inspired syntax has been adapted by so many vastly different languages, that at this point I'd recommend it as a default starting point when designing a new language, no matter what kind of language you're creating.

Code

All code examples for the series will be in this repository.

Code for the Ioke episode is available here.