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:
wranglewith a function we’ve defined.
wrangle "Bob" 42to apply the function to two arguments.
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:
if_then_else_can be applied using the syntax
if True then 42 else 43.
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.
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
There are a couple things happening here in the above:
layoutdefinition, we can reference the name of the operator (the
namereference), which tells the renderer to insert the name of the symbol. Names are still used both for convenience (it’s nice to be able to search by name, and communicate with other programmers using names) and if the function is not fully saturated.
arg1call). For each such argument, we indicate what the ambient precedence should be in that context. This will be used by the renderer to control where parentheses are needed. For left-associative operators, we don’t want to show parens for a left associated chain like
((a + b) + c) + d, so we only increase ambient precedence when descending into the right subtree. The renderer takes care of inserting parens if the ambient precedence exceeds the precedence of the current operator being rendered.
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:
Doctype specifies a responsive layout that can be displayed at different widths. Our infix operators will be rendered as either:
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