Wren is an small language meant for embedding that aims to displace Lua before JavaScript does it.
It's so committed to the embedding use case, that it doesn't come with any way to run it in the default package (brew install wren
) - that one is just an embeddable library. To actually run it you need to brew install wren-cli
, or write an app embedding wren. I think that might be going a bit too far. As a consequence of this philosophy, it has extremely minimal standard library, making it not really suitable for standalone use.
Its syntax feels about halfway between Ruby and JavaScript, which is totally reasonable.
It doesn't seem to be doing anything particularly innovative, but its main competitors are Lua and non-browser JavaScript, so it could win by just having fewer issues.
Hello, World! with Wren CLI
We can use "Wren CLI" to print Hello, World:
#!/usr/bin/env wren_cli
System.print("Hello, World!")
$ ./hello.wren
Hello, World!
It also provides a REPL, with ASCII art wren:
$ wren_cli
\\/"-
\_/ wren v0.4.0
> 2+2
4
> "Hello, World!"
Hello, World!
>
Embedded Wren
Wren's main focus is being embedded, and it has C API for embedding Wren in your C application. As it's all C API, you could also create a bridge between Wren and pretty much any other language. It's a real thing, I even used one between Ruby and Lua once for a build script for some Factorio mods.
Anyway, I won't be doing any embedding in this episode, and I'll treat Wren CLI as a representative environment where Wren code might live.
Unicode
Wren REPL will outright refuse to accept any non-ASCII characters, like if you try to write some accented characters in a string, this happens:
$ wren_cli
\\/"-
\_/ wren v0.4.0
> "Unhandled key-code [dec]: 196
> "
I usually test how languages handle Unicode conversion to upper or lower case, but Wren doesn't have any such functions.
Wren makes some really unusual choices with how it handles UTF-8 and bytes:
#!/usr/bin/env wren_cli
var name = "Alice"
System.print("Hello, %(name)")
System.print("Lengths are codePoints by default:")
System.print("Żółw".count)
System.print("🍰".count)
var s = "Żółw"
System.print("Bytes vs codePoints counts:")
System.print("%(s) - %(s.bytes.count) bytes")
System.print("%(s) - %(s.codePoints.count) code points")
for (i in 0..6) {
System.print("s[%(i)] = %(s[i])")
System.print("s[%(i)] byte = %(s.bytes[i])")
System.print("s[%(i)] code point = %(s.codePoints[i])")
}
$ ./unicode.wren
Hello, Alice
Lengths are codePoints by default:
4
1
Bytes vs codePoints counts:
Żółw - 7 bytes
Żółw - 4 code points
s[0] = Ż
s[0] byte = 197
s[0] code point = 379
s[1] = �
s[1] byte = 187
s[1] code point = -1
s[2] = ó
s[2] byte = 195
s[2] code point = 243
s[3] = �
s[3] byte = 179
s[3] code point = -1
s[4] = ł
s[4] byte = 197
s[4] code point = 322
s[5] = �
s[5] byte = 130
s[5] code point = -1
s[6] = w
s[6] byte = 119
s[6] code point = 119
Step by step:
- variables are declared with
var
- string interpolation uses
%()
- I keep complaining about how every language now has string interpolation, but every single one decided to use different syntax for it, and well, Wren picked yet another one. - Wren has JavaScript-style loops with Ruby-style ranges, so we can do
for (i in 0..6)
and it will iterate from0
to6
-
string.count
returns correct UTF-8 character count - you can access bytes for a string with
string.bytes
- or code points withstring.codePoints
And the wild part is how indexing works:
- even though
s.count
returns number of characters,s[i]
uses byte indexes -
s.bytes
acts like a normal array of bytes,[197, 187, 195, 179, 197, 130, 119]
,s[2]
meant byte at position2
(counting naturally from 0) -
s.codePoints
returns array of codePoints, but it has a bunch of-1
s inserted to align it with byte position, sos[2]
means "codepoint at byte position2
", not "codepoint at codepoint position 2". -
s.codePoints
is[379, -1, 243, -1, 322, -1, 199]
-
s[i]
returns a character (not byte), at byte positioni
- or garbage if that is not a valid start of a character
Julia is the only language I know which does something similar. I understand that this solution offers good performance and simplicity, but it must be a huge pain to actually use.
Lists
#!/usr/bin/env wren_cli
var a = [1, 2, 3, 4, 5]
System.print([1, 2, 3])
System.print(1..3)
System.print(a.map{|i| i*2})
System.print(a.map{|i| i*2}.toList)
System.print(a.where{|i| i%2 == 1}.toList)
System.print(a.reduce{|i,j| i+j})
System.print(a == a)
System.print([] == [])
$ ./lists.wren
[1, 2, 3]
1..3
instance of MapSequence
[2, 4, 6, 8, 10]
[1, 3, 5]
15
true
false
- lists have standard syntax
[]
- they support
.map
,.reduce
, and.where
with Ruby-style blocks - languages are slowly converging towards the same names for these, but as you can see fromwhere
, we're not quite there yet -
.map
and.where
returnMapSequence
not a list, so once you're done you need extra.toList
- Wren does not have structural
==
- it only works on primitive types, on lists it just checks for object identity, two lists with identical contents will not==
each other
Maps
#!/usr/bin/env wren_cli
var person = {
"name": "Alice",
"surname": "Smith",
"age": 25,
}
System.print("%(person["name"]) %(person["surname"]) is %(person["age"]) years old")
for (prop in person) {
System.print("Person's %(prop.key) is %(prop.value)")
}
System.print({} == {})
$ ./maps.wren
Alice Smith is 25 years old
Person's name is Alice
Person's surname is Smith
Person's age is 25
false
Maps behave pretty much as you'd expect. Iteration returns objects with .key
and .value
, in undetermined order, which is a nice API.
Unfortunately Wren doesn't have structural ==
for Maps either.
Classes
#!/usr/bin/env wren_cli
class Point {
x { _x }
y { _y }
construct new(x,y) {
_x = x
_y = y
}
toString { "Point(%(_x), %(_y))" }
+(other) { Point.new(_x + other.x, _y + other.y) }
==(other) { other.type == Point && x == other.x && y == other.y }
}
var a = Point.new(400, 60)
var b = Point.new(20, 9)
var c = a + b
var d = Point.new(420, 69)
System.print(a)
System.print(b)
System.print(c)
System.print(c == null)
System.print(c == d)
System.print(c != d)
$ ./classes.wren
Point(400, 60)
Point(20, 9)
Point(420, 69)
false
true
true
Step by step:
- like most things, syntax for classes is a mix between JavaScript and Ruby
- instance variables have
_
sigil - Wren has getters
.x
and zero argument methods.clear()
as separate things - it recommends to use()
form if method has side effects - that looks like a fairly unique design decision - you can override
toString
and various operators like+
- by default
==
only compares object identity - we can override
==
- interestingly that doesn't automatically override!=
so here we have simultaneouslyc == d
(using overriden structural==
) andc != d
(using default object identity). Ouch
Should you use Wren?
The language is brutally minimalistic. It also makes a lot of quirky choices, but each one seems to be motivated by seeking implementation simplicity and performance, traits that matter for its embedded use case.
So Wren gets a hard no from me for standalone use, but that's likely something even its creators would agree with.
For embedded use case, it actually looks like a very reasonable choice. I didn't really cover its C API, but it seems small and practical, and it compares quite well to its main competitors Lua and non-browser JavaScript, as well as other alternatives like Tcl, Guile, creating your application-specific language, or embedding a whole general purpose language like Ruby or Python.
I think Wren is a well designed language given tradeoffs it decided to make.
As Wren is still pre-1.0, here's what I'd recommend it to change, without compromising its goals:
- have some kind of module system for optional code, like a module with common string operations - not every application needs them, but it's better to make them available if you do
- really reconsider making
==
work as structural equality (or at least add===
or something that does full structural equality) - fix Wren CLI to support non-ASCII input
- maybe link
==
with!=
? That was definitely an unexpected quirk
Code
All code examples for the series will be in this repository.
Top comments (0)