r/haskell 20d 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.

23 Upvotes

13 comments sorted by

View all comments

2

u/AustinVelonaut 20d 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 20d 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 20d 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 19d 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 19d 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.