100 Languages Speedrun: Episode 90: YueScript
In the previous episode I reviewed MoonScript, "CoffeeScript for Lua", which unfortunately has not seen any releases since 2015.
What I didn't know about is that its fork YueScript is actively maintained, and with some extra features.
All MoonScript examples from the previous episode also work on YueScript, so I'll try a few new things.
Lua and all Lua-based languages suffer from extremely underpowered standard library. Instead of repeating the same points again, this time I'll try to use third party modules for things which really ought to be in the standard library, like Unicode support, dealing with files and strings, and so on.
With Lua library situation is a bit more complicated, as your end goal is often running your code in some existing program (like a video game), and not all libraries will work there, depending on Lua version they use, and if library has any C code dependencies etc. So "you can just install libraries" really does not excuse Lua's inadequate standard library, even if that would sort of work for other languages.
Hello, World!
You can install YueScript with luarocks install yuescript
.
To just run something we need to pass -e
to YueScript, as by default it compiles to Lua without executing:
#!/usr/bin/env yue -e
print "Hello, World!"
$ ./hello.yue
Hello, World!
FizzBuzz
We can do it just like in MoonScript, or we can also use |>
operator from Elixir to pipeline data. |>
is one of the big hits, and it's making its way into more and more languages.
I'm not saying this is a particularly good use case for |>
, but here it goes:
#!/usr/bin/env yue -e
for i = 1, 100
if i % 15 == 0
"FizzBuzz" |> print
elseif i % 5 == 0
"Buzz" |> print
elseif i % 3 == 0
"Fizz" |> print
else
i |> print
Unicode
Lua treats all strings as collections of bytes, which is ridiculous in this day and age, especially as its main use case is games, which all have to get translated into a lot of languages including usually Chinese.
Lua 5.3 (which is not what every Lua platform supports) added extremely minimal UTF-8 support - literally just "UTF-8 length" and "Nth UTF-8 character", but if we're on Lua 5.3+ we can get a slightly less wrong answer:
#!/usr/bin/env yue -e
"Hello" |> utf8.len |> print
"Żółw" |> utf8.len |> print
"💩" |> utf8.len |> print
"Żółw" |> string.upper |> print
"Żółw" |> string.lower |> print
$ ./unicode.yue
5
4
1
ŻółW
Żółw
Even this little doesn't work on LuaJIT:
$ yue unicode.yue
Built unicode.yue
$ luajit unicode.lua
luajit: unicode.lua:1: attempt to index global 'utf8' (a nil value)
stack traceback:
unicode.lua:1: in main chunk
[C]: at 0x01099eb300
Unicode with third party libraries
Let's try to use third party Unicode support with luarocks install luautf8
.
#!/usr/bin/env yue -e
utf8 = require "lua-utf8"
"Hello" |> utf8.len |> print
"Żółw" |> utf8.len |> print
"💩" |> utf8.len |> print
"Żółw" |> utf8.upper |> print
"Żółw" |> utf8.lower |> print
It works:
$ ./unicode2.yue
5
4
1
ŻÓŁW
żółw
Unfortunately that's a C library, so it doesn't work with LuaJIT, and it's not something you can use as a modder. So existence of a third party library is not an excuse for not just fixing the standard library.
Wordle
Here's slightly improved version of Wordle from the previous episode. Using split
from luarocks install string-split
, which does not leave leftover empty string at the end. YueScript also allows method calls with either backslash or slightly more conventional ::
.
#!/usr/bin/env yue -e
split = require "string.split"
readlines = (path) ->
file = io.open(path)
text = file::read("*a")
file::close!
split text, "\n"
random_element = (array) ->
array[math.random(#array)]
class Wordle
new: =>
@words = readlines("wordle-answers-alphabetical.txt")
report: (word, guess) =>
for i = 1, 5
letter = guess::sub(i,i)
if word::sub(i,i) == letter
io.write "🟩"
-- non-regexp string.contains?
elseif word::find(letter, 1, true)
io.write "🟨"
else
io.write "🟥"
io.write "\n"
__call: =>
word = random_element(@words)
guess = ""
while guess != word
io.write "Guess: "
guess = io.read!
if #guess == 5
@report(word, guess)
else
print "Guess must be 5 letters long"
game = Wordle()
game()
$ ./wordle.yue
Guess: crane
🟥🟥🟩🟥🟥
Guess: smash
🟨🟥🟩🟩🟥
Guess: blast
🟥🟩🟩🟩🟥
Guess: flask
🟥🟩🟩🟩🟥
Guess: glass
🟩🟩🟩🟩🟩
Functional Programming
MoonScript and YueScript have list comprehensions. YueScript additionaly has |>
operator. Lua lacks proper print
but we can install library luarocks install inspect
to get something usable. With all that put together, let's try doing some functional programming:
#!/usr/bin/env yue -e
inspect = require "inspect"
nums = [i for i = 1, 10]
evens = [i for i in *nums when i % 2 == 0]
nums |> inspect |> print
evens |> inspect |> print
$ ./functional.yue
{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }
{ 2, 4, 6, 8, 10 }
It's a start, but not exactly impressive. We don't even have map
, filter
, and reduce
, which would be the absolute minimum for functional programming.
More Functional Programming
Fortunately YueScript is flexible enough that we can define our own range
, map
, filter
, reduce
and so on.
#!/usr/bin/env yue -e
inspect = require "inspect"
range = (a, b) -> [x for x = a, b]
map = (list, f) -> [f(x) for x in *list]
filter = (list, f) -> [x for x in *list when f(x)]
reduce = (list, initial, f) ->
value = initial
for x in *list
value = f(value, x)
value
range(1, 10)
|> filter (x) -> x % 2 == 0
|> map (x) -> x * 10
|> inspect
|> print
range(1,10)
|> reduce 0, (a, b) -> a + b
|> print
$ ./functional2.yue
{ 20, 40, 60, 80, 100 }
55
This code might seem trivial, but if you look at generated Lua, you'll see some of the value YueScript provides:
local inspect = require("inspect")
local range
range = function(a, b)
local _accum_0 = { }
local _len_0 = 1
for x = a, b do
_accum_0[_len_0] = x
_len_0 = _len_0 + 1
end
return _accum_0
end
local map
map = function(list, f)
local _accum_0 = { }
local _len_0 = 1
for _index_0 = 1, #list do
local x = list[_index_0]
_accum_0[_len_0] = f(x)
_len_0 = _len_0 + 1
end
return _accum_0
end
local filter
filter = function(list, f)
local _accum_0 = { }
local _len_0 = 1
for _index_0 = 1, #list do
local x = list[_index_0]
if f(x) then
_accum_0[_len_0] = x
_len_0 = _len_0 + 1
end
end
return _accum_0
end
local reduce
reduce = function(list, initial, f)
local value = initial
for _index_0 = 1, #list do
local x = list[_index_0]
value = f(value, x)
end
return value
end
print(inspect(map(filter(range(1, 10), function(x)
return x % 2 == 0
end), function(x)
return x * 10
end)))
return print(reduce(range(1, 10), 0, function(a, b)
return a + b
end))
One downside of YueScript is that you get really bad error messages if you forget that *
in the list comprehensions. And overall quality of error messages is not great.
YAML style objects
YueScript provides nice syntax for YAML-style objects. This might seem trivial, and most programs don't really need that, but in specifically game mods, there's so much data of this kind:
#!/usr/bin/env yue -e
inspect = require "inspect"
person =
name: "Patricia"
age: 42
occupation: "programmer"
hobbies:
* "programming"
* "biking"
* "swimming"
friends:
* "John"
* "Mary"
* "Bob"
* "Sally"
person |> inspect |> print
$ ./yaml.yue
{
age = 42,
friends = { "John", "Mary", "Bob", "Sally" },
hobbies = { "programming", "biking", "swimming" },
name = "Patricia",
occupation = "programmer"
}
Here's another one:
#!/usr/bin/env yue -e
inspect = require "inspect"
-- data from Factorio wiki
fireArmor =
name: "fire-armor"
icons:
* icon: "fireArmor.png"
tint: {r: 1, g: 0, b: 0, a: 0.3}
resistances:
* type: "physical",
decrease: 6,
percent: 10
* type: "explosion",
decrease: 10,
percent: 30
* type: "acid",
decrease: 5,
percent: 30
* type: "fire",
decrease: 0,
percent: 100
fireArmor |> inspect |> print
$ ./yaml2.yue
{
icons = { {
icon = "fireArmor.png",
tint = {
a = 0.3,
b = 0,
g = 0,
r = 1
}
} },
name = "fire-armor",
resistances = { {
decrease = 6,
percent = 10,
type = "physical"
}, {
decrease = 10,
percent = 30,
type = "explosion"
}, {
decrease = 5,
percent = 30,
type = "acid"
}, {
decrease = 0,
percent = 100,
type = "fire"
} }
}
Macros
YueScript also has simple macro system, but it's all string-based, and YueScript is whitespace-sensitive, so I really can't see how it could work except for the most trivial macros.
#!/usr/bin/env yue -e
macro maybe = (code) -> "#{code} if math.random() < 0.5"
for n = 1,10
$maybe print n
$ ./macro.yue
1
3
6
8
10
And a few other things
To save you having to check the MoonScript episode, here are some examples that work perfectly well in YueScript as well:
#!/usr/bin/env yue -e
fib = (n) ->
if n <= 2
1
else
fib(n-1) + fib(n-2)
for n = 1,20
print "fib(#{n})=#{fib(n)}"
#!/usr/bin/env yue -e
class Person
new: (@name, @surname, @age) =>
__tostring: =>
"#{@name} #{@surname}"
maria = Person("Maria", "Ivanova", 25)
print "#{maria} is #{maria.age} years old"
#!/usr/bin/env yue -e
class Vector
new: (@x, @y) =>
__tostring: => "<#{@x},#{@y}>"
__add: (other) => Vector(@x + other.x, @y + other.y)
__eq: (other) =>
@__class == other.__class and @x == other.x and @y == other.y
a = Vector(20, 60)
b = Vector(400, 9)
c = a + b
print a
print b
print c
print c == Vector(420, 69)
Should you use YueScript?
I didn't have terribly high expectations coming in, but this is the best version of Lua I've seen so far. YueScript can't fix fundamental issues with Lua (maybe some Lua 6 will?), but between much nicer syntax, and copious use of third party libraries for stuff Lua should have included, you can have a drastically better experience than coding in plain Lua. Unfortunately many such libraries require compiling C code and won't run in games.
I think as a game modder, you should use YueScript instead of Lua for your mods.
As for embedding in new apps, I don't think Lua VM with any language is a great choice, due to major missing components.
One thing YueScript really should do is get VSCode plugin. There's one for MoonScript (which I used while writing this episode), so it shouldn't be much work to just tweak it a bit.
An even more interesting thing would be some lua2yue reverse compiler. js2coffee was my favorite tool back when I was using CoffeeScript. That however, would be a fairly big project.
Code
All code examples for the series will be in this repository.