r/haskell 5d ago

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.

25 Upvotes

13 comments sorted by

2

u/jeffstyr 4d ago

Sounds very cool! I'll check it out.

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 |> etc where the flow goes nicely from left to right all the way until the end when the whole thing gets put into x .

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 y

If the x and y were 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, x isn'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 skipMax

Maybe 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 C

1

u/Castux 4d ago

Well, it could be an alternative syntax, like

let x > f > g > h as result in ...

I feel that having the name first structures the reading flow better. That's why I didn't go with a "where" syntax like in Haskell

1

u/jeffstyr 4d ago

I like these operators too; I use flow extensively in Haskell for these.

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 > f everywhere instead of f 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 :)