2

How do you define functions and values depending on each other in Standard ML?

The following program:

val cmds = [("help", cmd_help)];

fun cmd_help () = List.app (fn cmd => print (#1 cmd ^ "\n")) cmds;

cmd_help ();

Does not compile:

$ mlton example.sml
Error: example.sml 1.22-1.29.
  Undefined variable: cmd_help.

Is it possible to define cmds and cmd_help such that they both know about each other and are also exposed as top-level definitions to the rest of the program?

Lassi
  • 3,522
  • 3
  • 22
  • 34

2 Answers2

3

Interesting question, the most obvious way would be to make cmds a unitary function, and then call that in cmd_help just like a normal pair of recursive functions.

fun cmds () = [("help", cmd_help)]
and cmd_help () = List.app (fn cmd => print ((#1 cmd) ^ "\n")) (cmds ());
cmd_help ();

One would expect that you could reverse the situation, treating cmds_help as a value. Indeed the syntax even appears to parse.

val a = ()
and b = fn () => ()

However, it doesn't actually compile when you add mutual recursion into the works (I tried the following with MLton and smlnj). Manually declaring types because the inference wasn't doing particularly well.

val cmds_v : (string * (unit -> unit)) list = [("help", cmd_help_v)]
and cmd_help_v : unit -> unit = fn () => List.app (fn (cmd: string * (unit -> unit)) => print ((#1 cmd) ^ "\n")) (cmds_v);

This last case results in:

Error: unbound variable or constructor: cmds_v

Error: unbound variable or constructor: cmd_help_v

So the question now is, why doesn't this work If we look at the section Value Bindings, on pg 42 of The Definition of Standard ML (Revised) in the footnote 25 it says

(25) When the option is present we have Dom VE ∩ Dom VE′ = ∅ by the syntactic restrictions.

So while and for functions allows mutual recursion, and for values allows ensures that the values environments are disjoint.

Alas, I don't know how to do this without promoting a value to a function.

matt
  • 5,364
  • 1
  • 25
  • 25
  • Excellent answer. I had no idea the rabbit hole goes this deep. The restriction on values is probably to stop us from defining things like `val a = b and b = a`. The functions `fun a () = b () and b () = a ()` also cause an infinite recursion, but since they are evaluated at runtime, it doesn't matter to the compiler. – Lassi Aug 16 '20 at 18:48
  • If you could define mutually recursive values at compile time, the language would probably need to have some feature to guarantee that any such recursion terminates (_total_ functional programming is the term unless I've misunderstood something). – Lassi Aug 16 '20 at 18:50
  • 3
    Some comments here: `fun` is shorthand for `val rec`, which _does_ allow mutual recursion. However, it requires all RHSs to be function expressions. It would be possible to relax this to any syntactic value. For example, OCaml allows `let rec fs = [f] and f x = x`. However, this has some serious drawbacks, like, you can now construct cyclic lists, i.e., break termination of certain functions. FWIW, the usual workaround is to "tie the knot" manually using a reference: `val cmds = ref []; fun cmd () = ...; val _ = cmds := [cmd]`. – Andreas Rossberg Aug 17 '20 at 16:13
  • Yeah, there are obvious drawbacks, One reason I find the question interesting is because you can declare a non-self-recursive function with `val id = fn x => x`, so if you *could* declare mutually recursive values, the syntax could then express functions which are mutually recursive but not self recursive. It seems like both ocaml and SML tie self recursion and mutual recursion, such that all mutually recursive expressions are also given a self recursor. – matt Aug 17 '20 at 22:05
  • I didn't know this either, so thanks for teaching me. :) – sshine Aug 19 '20 at 10:44
1

I think the best solution is to wrap the function in a ref access, like so:

val cmd_help_ref : (unit -> unit) ref
    = ref (fn _ => raise Fail "cmd_help not bound");

fun cmd_help () = !cmd_help_ref ();

val cmds = [("help", cmd_help)];

cmd_help_ref := fn () => List.app (fn cmd => print (#1 cmd ^ "\n")) cmds;

cmd_help ();
Skyler Soss
  • 101
  • 1
  • 6