Delving into language design: what I've learned so far
As can be read in my first blog post, I intend to build an Elm-like language with some unique design choices – and the community has taught me some valuable things!
TLDR: My design seems akin to cutting-edge programming languages. I have a functional proof-of-concept! I'm not content with the syntax yet.
My idea isn't original – but it seems new
One of the most intriguing parallels to selective mutability I've discovered is the “resurrection hypothesis” mentioned in a 2019 paper called Counting Immutable Beans. The resurrection hypothesis mentions the idea that many objects die just before the creation of an object of the same kind.
The map
function is a great example to this:
type Tree a = Leaf a | Node (Tree a) (Tree a)
map : (a -> b) -> Tree a -> Tree b
map f tree =
case tree of
Leaf x ->
Leaf (f x)
Node tree1 tree2 ->
Node (map f tree1) (map f tree2)
If the language is purely immutable, then you might use twice the memory necessary: you would traverse through the tree, build a new tree with the identical structure, and then discard the old one. But imagine that the update
function of our MUV-system looks like this:
type alias Model = { name : String, tree : Tree Int }
update : Int -> Model -> Model
update n model =
{ model | tree = map (\x -> x * 2 } model.tree }
Most immutable languages would duplicate the tree and discard the old one, effectively doubling memory usage. But with selective mutability, the tree could be updated in-place.
There's also other programming languages developing similar ideas. Research language Koka uses Perceus, an algorithm that offers reference counting in a way that avoids a garbage collector. Similarly, Neut does what they call static memory management, where they find malloc/free pairs of matching sizes to optimize around the resurrection hypothesis.
A proof-of-concept works – for now
As an experiment, I have built a proof-of-concept transpiled version. A major challenge with catching memory problems is that most OS systems aren't built for catching memory issues. From my understanding, C, LLVM and Rust typically rely on the OS to manage the stack & heap. If there's an overflow, or some other problem, the OS terminates the program, reporting a segmentation fault. Not very helpful!
As a result, I have designed my own stack/heap system in a C program. Similar to a VM, the code runs in a single block of memory that's assigned to the program on startup. It functions reliably, regardless of available memory size.
For now, this snippet represents the decompiled version of the file low.c:
main =
h "Kaokkokos shall prevail by the hands of Alkbaard!"
h : String -> String
h x = f ( g x )
f : String -> String
f = String.upper
g : String -> String
g = Console.print -- currently an identity function with side-effects
This proof-of-concept shows that a memory-aware runtime is feasible, though Mem.withDefault handling is still pending implementation.
Memory-aware language design might need some changes
There's two major challenges with a design using =>
operations. It's a difficult concept to understand, and it limits the capability for the language to compile to environments where memory cannot managed.
As a result, it might be rewarding to design the types in a Mem
module that is only usable in environments where memory CAN be managed. This has several downsides to be considered, but the confusion of the =>
operation might not necessarily be a better directly. For example, consider the following two functions:
-- Example 1
foo1 : Foo => Foo -> Foo
foo1 x =
(\y -> bar x y )
|> Mem.withDefault identity
-- Example 2
foo2 : Foo -> Foo => Foo
foo2 x y =
bar x y
|> Mem.withDefault defaultFoo
Both functions are essentially different, offering different guarantees in different scenarios. While foo1
is guaranteeing to return a Foo
type when two Foo
types have been inserted, foo2
guarantees to return a Foo -> Foo
function after one Foo
type has been inserted.
This guarantee sounds relatively reasonable when you look at the code, but how much effort would it cost to write a fully memory aware function?
foo : Foo => Foo => Foo
foo x =
(\y -> bar x y |> Mem.withDefault defaultFoo )
|> Mem.withDefault identity
This is a rather unappealing way to write code, and quite difficult to read. This might need some reworking.
Conclusion
I have learned a few more concepts and the development seems to go rather well! I am encountering less hurdles than expected, and the design seems manageable.
As with the previous post, I am very much open to ideas. Let me know if you have any thoughts to share!
#foss #functional #languagedesign
Older post <<<