Skip to content
Bill Hails edited this page Aug 13, 2024 · 23 revisions

Printing

The print() command will print a value.

Typedefs will print showing their constructors, so true and false will display as true and false etc. provided that the print() command is in the scope of the typedef. Outside of that scope it will print a generic vector representation of the structure without type information (there is no run-time type information). For the same reason, print won't be able to infer the type of an argument within a polymorphic function, and will fall back to a generic representation.

User-defined Print Functions

The keyword print can also be used to define a function that will print a user-defined type. The syntax is: print <typename> <function> For example you might define a dictionary type:

typedef dictionary(#t, #u) { leaf | branch(dictionary(#t, #u), #(#t, #u), dictionary(#t, #u)) }

And you might implement an insert function for it. A generic print function would normally be defined automatically for that new type (see Implementation below). However that generic print function will often reveal unnecessary details about your implementation to your users, for example

print(insert(1, "Hello", insert(2, goodbye, leaf)));

Would produce something like:

branch(leaf, #(1, "hello"), branch(leaf, #(2, "goodbye"), leaf))

You can override that by providing your own print function, which must be at the same level as (alongside) the typedef, as follows:

print dictionary(pt, pu, d) {
    let
        fn pd {
            (leaf) | { [] }
            (branch(l, #(t, u), r)) {
                pd(l);
                puts("  ");
                pt(t);
                puts(": ");
                pu(u);
                puts("\n");
                pd(r);
                []
            }
        }
    in
        puts("{\n");
        pd(d);
        puts("}");
        d;
}

After which print(insert(1, "Hello", insert(2, goodbye, leaf))) will produce:

{
  1: "hello"
  2: "goodbye"
}

Note the arguments to the print function: there is one argument for each abstract type, itself a function of one argument that will print that type (without a trailing newline). They are followed by the actual object to be printed. Since your print function can be passed as argument to other print functions (if your dictionary was used as a component in a larger type,) it too should refrain from generating a final trailing newline. The final newline is appended by the top-level print() command, or by users of your function. It is also considered good form to return the value that was printed.

Note also the use of some until-now undocumented helper functions that are built in to the language

  • puts takes a single string and prints it, without quotes, expanding escape sequences like "\n" etc.
  • putc takes a single character and prints it, without quotes, expanding escape sequences like '\n' etc. This is more efficient for single characters.
  • putn prints a single number.
  • putv will print the unadorned raw form of any value.

Implementation

Although out of scope for a description of the language, I think the implementation is pretty cool, so I'll talk about it a bit here.

Consider a fairly complex typedef like a tree:

typedef tree(#k, #v) { leaf | node(tree(#k, #v), #k, #v, tree(#k, #v)) }

Printer Generation

in order to print this, we need to generate a function that knows its structure, and call that.

However we can't know the types of #k and #v at the time we're generating the function, so it will need to accept functions to print those types when we know what they are. This is a great candidate for currying. If we arrange that all such print functions take the thing to be printed last, for example:

fn print_tree {
  (printK, printV, tree(l, k, v, r)) {
    puts("tree(")
    print_tree(printK, printV, l);
    puts(", ");
    printK(k);
    puts(", ");
    printV(v);
    puts(", ");
    print_tree(printK, printV, r);
    puts(")");
  }
  (_, _, leaf) { puts("leaf") }
}

This stage I'm calling "printer generation" - abstract functions are created with one argument for each abstract type, plus the thing to be printed.

If there exists a user-defined print function however, a default printer is not generated.

Printer Compilation

Then at the point the print function is being type-checked, it can be invoked with all but the last argument, specialising it for the particular types in that use case and returning a function of one argument that will print that argument.

This stage I'm calling "printer compilation" the actual concrete printer function is "compiled" from simpler functions.

printer = print_tree(print_string, print_int)

The advantage of doing it this way is that those argument functions have exactly the same behaviour, so when printing a tree of strings to lists of foo it's just

printer = print_tree(print_string, print_list(print_foo))

And the type checker helpfully does all of this for you at the point of the print call, while the types are known.

Just to give a better idea of how this is all working, the intermediate (ANF) code to print a list of tuples of ints and strings:

[#("anne", 1), #("betty", 2)]

Looks like

((lambda (x$0)
   (let (anf$1
         (let (anf$3 (print$tuple$2 print$string print$int))
           (print$list anf$3)))
     (let (anf$2 (anf$1 x$0))
       (let (anf$0 (putc "\n")) x$0))))
 value)

(value is the value to print)

Translating that back into F-Natural, it would look something like:

fn (x0) {
  print_list(print_tuple_2(print_string, print_int))(x0)
}(value)

Next: Here