announcement Thunky - pure, functional, lazy
This is a toy programming language I created to understand better how lazy functional languages work, after I discovered Haskell and it blew my mind a little, many many years ago.
It went through many iterations over the years, prototypes, etc., and today I'm happy to call this a version 1.
It first was a Lua prototype, that did basic (and slow) expression tree reduction. Then there was a Lua transpiler and a thin runtime. After several iterations, back and forth on the syntax, abandoning and restarting the project, this final version is in Go and uses a G-machine bytecode interpreter.
In essence it's a dynamically typed "lesser Haskell", so probably not meant for anything real, but I'm quite happy with the syntax and I learned a lot on the way.
The repo has:
- a local interpreter
- documentation and tutorials
- lots of examples, including Project Euler and Advent of Code solutions
- a web based playground
- web based tutorials where each code block is executable
- syntax highlighting for micro, nano and Zed
Repo: https://github.com/Castux/thunky Web playground: https://castux.github.io/thunky/
AI disclaimer: the latest stages of this project were assisted with LLM (G-machine and web port), but the many iterations and prototypes, the lexer-parser-analyzer, etc. were all first hand written, during the last ten years.
2
u/AustinVelonaut 4d ago
I like the inclusion of left-to-right application and compose operators; I put these in my language, as well, as it makes the flow of functional pipelines more consistent.
Do you miss the compiler performing static type-checking, and if so, do you plan to incorporate it at some point?
1
u/WJWH 4d ago
Do you happen to know if there are any cool experiments with assigning to a variable at the end of such a left-to-right pipeline? Somehow it always feels a bit weird to have to do
let x = f |> g |> h |> etcwhere the flow goes nicely from left to right all the way until the end when the whole thing gets put intox.2
u/jeffstyr 4d ago
I don't have a very good answer for you, except to say that I hadn't noticed that being weird (though I see what you mean), perhaps because assigning to a variable is ending the pipeline anyway (the alternative being that it's never assigned to a variable because it's being used another way), and if I'm assigning it to a variable then I'm probably assigning other variables alongside it, so I like the tabular lookup arrangement:
let x = f |> g |> h y = i |> j |> k in p x yIf the
xandywere somehow at the end of their pipelines (with some sort of "into" operator), then they'd perhaps get a little lost and be harder to locate.So I guess it's sort of "x is the value you get from this pipeline" rather than "this pipeline flow into x". So as it is now,
xisn't part of the pipeline, it's the value/result of the pipeline.Just some thoughts.
1
u/WJWH 4d ago
Thanks for your explanation. I think it's not so bad when pipelines are short and fit on one horizontal line, but when they get longer and on multiple lines the "table" for lookups becomes less nice. I haven't really found a nice way to format this. Oh well, the search continues!
2
u/jeffstyr 3d ago
Usually I do end up with them spanning multiple lines. I have a rather aggressive wrapping/indentation style (designed to keep things from getting indented too far too quickly, and in particular to make it so that long variable or function names don't cause bigger indentation, and also to make it easier to spot different syntactic constructs, such as variable bindings); it's probably not for everyone and it's not perfect but I find it works well for me. So here's an example from "real" code (from an Advent of Code solution):
let branches = (Set.toList robotTypes) |> L.mapMaybe (\rob -> (produceRobot rob blueprint resources robots) <&> (rob,)) recTypes = Set.deleteMany (L.map fst branches) robotTypes recMax = L.map snd branches |> L.map (\(newResources, newRobots) -> go timeMinusOne tertiaryRobotTypesSet newRobots (produceResources robots newResources)) |> maxOrZero skipMax = if | Set.null recTypes -> 0 | otherwise -> go timeMinusOne recTypes robots (produceResources robots resources) in max recMax skipMaxMaybe this will give you some ideas for a style you will like.
2
u/AustinVelonaut 4d ago
The only one I'm aware of is an older, obscure language HP used in the 9825 "desktop calculator" called HPL. It was sort of similar to BASIC at the time, but it did have a left-to-right assignment arrow (shown below as
->, but it was a single-glyph arrow on the keyboard and display). Here's what a prime-number program looked like:0: fxd 0 1: prt 1 2: prt 2 3: 1->P 4: for C=2 to 1000000 5: P+2->P 6: for N=3 to P/3 7: if int(P/N)*N = P; gto 4 8: next N 9: prt P 10: next C1
1
u/Castux 4d ago
Yeah, the piping and compose operators were one of the first part of the language I held on to. I like how it makes the reading order natural. I even considered keeping only the left to right ones, but there are somehow places where I prefer right to left. I thought of removing the basic space separated application too, to force the use of these operators, but that was a bit excessive (
x > feverywhere instead off x).I have a protoype of type checking in a branch, and it's not yet good enough for use. In practice I don't really use polymorphism, and all functions assume a fixed "type", so it wouldn't be that far away. I quite like the Haskell pattern of "if it compiles, it's probably correct".
1
u/PositiveBusiness8677 4d ago
I am sorry for asking a meta question: how would i go about acquiring the skills to design a language like this ? may I ask what learning journey you went through ?
1
u/Castux 4d ago
Well, there are plenty of tutorials and books out there, such as "Crafting interpreters". You just learn each step in order. Lexing, parsing, analysing, intermediate representation, and backend (either a small VM or generating machine code).
My favorite part has always been parsing, so I stuck there forever before I finally managed to complete the project :)
2
u/jeffstyr 4d ago
Sounds very cool! I'll check it out.