BranchTaken

Hemlock language insights

Acyclic Mutually Recursive Closures

Ouroboros

Hemlock is a mostly pure functional programming language, and it absolutely prohibits reference cycles in immutable data. To someone steeped in imperative programming idioms this might seem like an insurmountable impediment to getting actual work done. I used to think so, but have in recent years shifted to preferring immutability and acyclicity when at all feasible. And there are some amazing optimization opportunities in the absence of cycles, particularly with regard to parallel garbage collection. That’s a topic for another time; this post is about how Hemlock enforces acyclicity.

It’s easy to create a reference cycle using mutation.

type rec t: &t = {
    x: uns
    &next: option t
  }

a = {x=42; next=None}
b = {x=13; next=Some a}
# Tie the knot.
a.next := Some b

How would we create an immutable reference cycle though? We would need some form of forward/mutual declaration. Imagine for a moment that Hemlock allows the following (it does not).

type rec t: t = {
    x: uns
    next: t
  }

# Poof, a knot.
a = {x=42; next=b}
  also b = {x=13; next=a}

Disaster is averted simply by omitting such constructs from Hemlock. But Hemlock does provide one seemingly dangerous construct: mutual recursion.

rec odd x =
    match x > 0 with
      | false -> "odd"
      | true -> even (x - 1)
  also even x =
    match x > 0 with
      | false -> "even"
      | true -> odd (x - 1)
f = even
f 42 # -> "even"
f 13 # -> "odd"

At the time the odd closure is created, even isn’t yet defined. A straightforward implementation would just mutate odd once even exists. Or the implementation could treat even as a “free variable” in the odd closure. Let’s look at a manual implementation of this that uses partial application to allocate closures and see why it’s a form of cycle-inducing mutation.

type odd_closure: &odd_closure = {
    &even: uns -> string
  }

odd_func {even} x =
    match x > 0 with
      | false -> "odd"
      | true -> even (x - 1)

type even_closure: &even_closure = {
    &odd: uns -> string
  }

even_func {odd} x =
    match x > 0 with
      | false -> "even"
      | true -> odd (x - 1)

dummy_func _x =
    ""

# Abracadabra, a Gordian knot!
even = {odd=dummy_func}
even.odd := odd_func even
odd = {even=dummy_func}
odd.even := even_func odd

Okay, let’s do something sightly more clever that has a long enough name even the German language would be moderately proud: defunctionalization.

type selector =
  | Odd of uns
  | Even of uns

rec odd_even selector =
    match selector with
      | Odd x ->
        match x > 0 with
          | false -> "odd"
          | true -> odd_even (Even (x - 1))
      | Even x ->
        match x > 0 with
          | false -> "even"
          | true -> odd_even (Odd (x - 1))

odd x =
    odd_even (Odd x)

even x =
    odd_even (Even x)

It is possible to generate machine code that is incrementally faster than variant dispatch, but this captures the essence of the approach, and it can always convert mutually recursive functions to a form that avoids reference cycles. Thank goodness, Hemlock programs can use mutual recursion without restriction!