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.
Top comments (5)
I'm glad you found and even liked YouScript!
Another thing I love about Lua, but didn't mentioned in first comment under Lua overview, tables can accept anything as a key, as opposed to only strings in JS.
LuaJIT has compatible with Lua 5.1 C API, I even wrote bit of C for it back in days. Also LuaJIT has
ffi
to access C functions from Lua side. So if doesn't work - hmm, strange, should work.Are you sure modders cannot use C or C++? Popular game engines, like Unity and others, can use C.
I tried to install it with
luarocks --lua-version 5.1 install luautf8
but that didn't work with LuaJIT. There could be a way to do this.Game developers can use C or C++, but most games would not allow modders to distribute compiled binaries with mods on any mod portal for security reasons, and many games are DRM-locked and really hate when people mess with their binaries to make such things work. Lua or such are sandboxed so it's fine. It all varies from game to game, but I'd expect that pure-lua libraries would work but C libraries usually wouldn't.
Also most languages allow arbitrary hash keys. ES6+ JavaScript as well, you just need to use
Map
not{}
.I've been thinking for a while why this fundamental poo issue of
"💩".length == 2
is there for ages in many languagesarguably, it's not a big issue, of course sometimes it may be a problem. But for some reason for many years in JS community nobody speaks about this problem, like there is no problem at all, and in my experience I didn't encounter on this as if it was a problem, despite of making various text boxes, sometimes rich text editors.
changing how string works will break backward compatibility, and break it really hard, so this cannot be changed in already existing language
I never had any problem before with counting chars. But it really sucks, after thinking. If we take database column with limited size, database can count poo as 4 bytes or as 1 char (I tested length('💩') and char_length('💩')). Then why the heck it's 2 in JS and so many languages including Lua? This can easily cause little unpleasant bugs when saving to db.
None of the issues I mention are individually big and you can absolutely work around them, it's just the big theory behind this series that if a language has a lot of issues with extremely basic stuff, there will be a lot more coming for more complex stuff and you'll spend more time working around issues than coding the actual thing. Incidental complexity will eat you.
Maybe there are languages which break this pattern (a lot of issues early but totally amazing later; or amazing early but huge problems later), but tbh I'm not sure which languages could be described this way. (unless you count installation issues, like a lot of very nice languages are huge pain to install on Windows; so I don't mention that).
Ruby 1➡️2 and Python 2➡️3 took the pains of breaking backwards compatibility to support Unicode properly, and it was definitely worth it.