The underscore-dot notation

We have already seen "The Power of the Dot" and how it can make programming easier when referring to functions that are in the scope of a module, a data type, or a type class.

We have seen usages like below where before the dot either the explicit scope of the data type (TextArea) is referenced or a value thereof (inputArea).

Referencing a scope or a value before the dot
-- dot notation on scope
TextArea.getCaretPosition inputArea
-- dot notation on reference

The underscore-dot notation allow us to be even more succinct.

Introducing the underscore-dot

Let’s suppose that we are in a situation where we know that inputArea is of type TextArea. The simplest example is an explicit declaration.

Explicit declaration
insertionPoint :: TextArea -> JFX Int
insertionPoint inputArea = inputArea.getCaretPosition

We see that inputArea is somewhat duplicated and the underscore-dot notation resolves that redundancy.

Underscore-dot to reference the unbound variable
insertionPoint :: TextArea -> JFX Int
insertionPoint = _.getCaretPosition
Side note

Remember that a function definition is just giving a name to a lambda expression. The function definition f x = x and the lambda declaration f = \x → x are equivalent.

In other words, the underscore-dot notation just replaces

Lambda term
\inputArea -> inputArea.getCaretPosition


Underscore-dot term

Let’s see more places where this is useful.

More Usages

For getting the name of the current thread, we have a variety of notational options.

Many styles to get to the current thread name
-- do notation
    thread <- Thread.current()
    name   <- thread.getName

-- lambda with formal parameter
Thread.current() >>= \t -> t.getName

-- lambda with partial application
-- (needs 'Thread' as explicit scope)
Thread.current() >>= Thread.getName

-- underscore-dot notation
Thread.current() >>= _.getName

The underscore-dot notation is also nice to use for higher-order functions where one would otherwise use sectioning or partial application. Let’s try this with the classical map function and the usage of mapping a list of thread values to their names.

Mapping threads to their names
map _.getName threads

Even the record-syntax can profit from using the underscore-dot notation, particularly when updating or modifying a record. Assume we have Person record with a name field. Modifying a person record just means passing in a person record and returning an updated one.

Updating a record with the underscore-dot notation
data Person = Person {name :: String}
setName :: String -> State Person ()
setName newName = do
    State.modify _.{name = newName}

Final Thoughts

One can only use the underscore in combination with an immediately following dot. A free-standing underscore is not allowed as a means of referring to a yet unbound variable. This is very much unlike other languages.

In every function or lambda term there is at most one such unbound variable that the underscore can refer to since, technically, functions and lambda terms in Frege and Haskell only ever take one parameter. Any additional unbound variables remain unbound like in the example below.

Notational options with more parameters
String.startsWith "Dierk" "D"
"Dierk".startsWith "D"
map (\s -> s.startsWith "D") ["Ingo","Dierk"]
map (_.startsWith "D") ["Ingo","Dierk"]

Furthermore, underscores can appear in a nested fashion.

Nesting with underscore-dot notation
data Person  = Person  {addr :: Address}
data Address = Address {street :: String}
setStreet :: String -> State Person ()
setStreet newStreet = do
    State.modify _.{addr <- _.{street = newStreet} }

The first underscore refers to a Person, the second one to its Address and there can be absolutely no question about which underscore refers to which type.

However, if you ever find it difficult to "decipher" the meaning of an underscore in your code, you can replace it with an explicit parameter any time.

Replace back with an explicitly named parameter
    State.modify (\p -> p.{addr <- (\a -> a.{street = newStreet} ) } )

Future IDE support could easily provide refactorings that switch between the two notations. Code completion after _. would be another interesting IDE feature.

Comparison to Haskell

The way that Frege is handling the dot is not available in Haskell. One can see it as a special syntax extension.

This extension has many benefits. Between them are:

  • terse notation as seen above that is familiar to Java programmers

  • typically plays well with Java-like APIs and their native definitions

  • enables IDE code completion

  • allows type-directed name resolution

results matching ""

    No results matching ""