next-generation programming platform, currently in development
about
help fund the project
swag

Twitter . GitHub . RSS

Richer rendering of function application



A couple quick updates:

Suppose I’ve written a function:

wrangle :: Text -> Age -> Text
wrangle name age = ...

The implementation of wrangle exists apart from that name. The wrangle name age = ... is actually doing two things:

As programmers, we have very little control over how function application is rendered (and/or parsed, if you are using a text editor). Your options:

These constraints on layout are completely artificial, but we still keep using them in our programming environments. Agda’s mixfix symbols might seem quite expressive, but why should we be excited that we get to describe layout of function application using bad ascii (or unicode!) art? There’s a whole world of possibilities for how function application can be rendered. For instance:

As an example of this third point, take Google’s mortgage calculator. We have the following:

mortgageCalculator : Amount -> InterestRate -> Period -> (Amount,Amount)
mortgageCalculator total rate period = ...

The widget is just a little spreadsheet. The top cell is a rendering of the expression mortgageCalculator 100k 3.92% 30yrs, whose arguments the user can edit, and the bottom cell is the computed result of that expression. There’s a lot of formatting around this to make it look nice and make its use more self-explanatory, but this is “just” window dressing.

Aside: Also see this earlier post on viewing a program as a UI.

Document formatted operators (DFOs)

When you define a function, you should have arbitrary control over how function application is rendered for your definition. You should not have to pick from a fixed menu of layouts, nor should you have to approximate your desired layout via ascii or unicode art.

In Unison, when you give a definition, you can associate an arbitrary document with blanks in it, one blank for each argument that is to be captured. Here are the main types:

newtype Precedence = Precedence Int deriving (Eq,Ord)
newtype Var = Arg Int deriving (Eq,Ord)
data Segment = Slot Var Precedence | Text Text

data DFO = DFO (Doc Segment (Maybe Var)) Precedence

Where Doc is an annotated layout description that can be rendered at various widths. Here’s a definition of postfix operators:

hugPostfix1 :: Precedence -> DFO
hugPostfix1 prec = DFO (D.docs [arg1 prec, name]) prec

arg1 :: Precedence -> Doc Segment (Maybe Var)
arg1 p = toDoc $ Slot (Arg 1) p

name :: Doc Segment (Maybe Var)
name = toDoc $ Slot (Arg 0) high

percent :: Symbol DFO
percent = Symbol.annotate hugPostfix1 (Symbol.named "%")

With this in place, we can use percent for a function definition’s symbol, and when applied to an argument x in the editor, it will be rendered as x%. Nice!

There are a couple things happening here in the above:

Here’s a function for building infix operators:

binary :: Associativity -> Precedence -> DFO
binary assoc prec =
  DFO layout prec
  where
  deltaL p | assoc == AssociateL || assoc == Associative = p
  deltaL p = increase p
  deltaR p | assoc == AssociateR || assoc == Associative = p
  deltaR p = increase p
  layout = D.docs
    [ arg1 (deltaL prec), D.breakable " ", name, text " "
    , arg2 (deltaR prec) ]

There are a couple interesting things happening here:

a + b

OR

a
+ b

The overall layout function also handles chains of operators, so that a + b + c + d renders either all on one line, or as:

a
+ b
+ c
+ d

… if it doesn’t fit.

This sounds complicated, but the main layout function is actually only about 50 lines of code to prettyprint arbitrary Unison types using DFOs. The view function just produces a description, which can easily be interpreted to various targets like plain text or the DOM.

I haven’t implemented the term viewing function but I expect it will be similar in complexity.

I hope to have a demo of all this running in the browser very soon!

comments powered by Disqus