Skip to content

Instantly share code, notes, and snippets.

@evincarofautumn
Last active April 20, 2023 21:16
Show Gist options
  • Star 12 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save evincarofautumn/9cb3fb0197d2cfc1bc6fe88f7827216a to your computer and use it in GitHub Desktop.
Save evincarofautumn/9cb3fb0197d2cfc1bc6fe88f7827216a to your computer and use it in GitHub Desktop.
Thoughts on an InlineDoBind extension

Thoughts on an InlineDoBind extension

An expression beginning with a left arrow (<-) inside a do block statement is desugared to a monadic binding. This is syntactically a superset of existing Haskell, including extensions. It admits a clean notation that subsumes existing patterns and comes with few downsides.

Examples

do
  f (<- x) (<- y)
-- ===
do
  A <- x
  B <- y
  f A B

(Here, A and B are assumed to be fresh variables.)

Like applicative notation, this makes useless intermediate names easier to avoid, leading to code that’s more stable under certain refactorings, in the same manner as point-free style.

Moreover, desugared binds cannot refer to one another, because their bindings are not named. This plays very nicely with ApplicativeDo desugaring as a result.

do
  f (<- x) (<- y)
-- ===
join $ f <$> x <*> y

The greatest benefit is a natural inline style of binding without applicative operator soup.

do
  f (<- a) b (<- c) d
-- ===
join $ f <$> a <*> pure b <$> c <*> pure d

The common solution to this is to reorder the arguments of f to take the pure bindings first, in which case the applicative notation is more concise, but the inline-bind notation is comparable in size and more legible.

do
  f b d (<- a) (<- c)
-- ===
join $ f b d <$> a <*> c

It brings Haskell syntax closer to mainstream languages with things like async / await, as well as Go’s notation for channels.

do
  let (a1, a2) = (<- async (getURL url1), <- async (getURL url2))
  let (page1, page2) = (<- wait a1, <- wait a2)
  ...
-- ===
do
  A <- (,) <$> async (getURL url1) <*> async (getURL url2)
  let (a1, a2) = A
  B <- (,) <$> wait a1 <*> wait a2
  let (page1, page2) = B
  ...

Note that this doesn’t preserve the desirable property that let bindings are trivially known to be pure, because they may introduce monadic bindings before them.

Details

The desugaring relates expressions to their innermost enclosing do. So this:

do
  f (g (<- x))
-- ===
do
  A <- x
  f (g A)
-- ===
f . g =<< x

Is different from this:

do
  f $ do
    g (<- x)
-- ===
do
  f $ do
    A <- x
    g A
-- ===
f (g =<< x)

Therefore, do is given slightly more significance, leading to code that’s potentially less stable under certain refactorings such as motion/insertion/deletion of do blocks.

Desugaring proceeds from left to right, i.e., depth-first in the case of nested inline bindings:

do
  process (<- (<- getAction) (<- getArgument)) (<- getConfig)
-- ===
do
  Result <- (<- getAction) (<- getArgument)
  Config <- getConfig
  process Result Config
-- ===
do
  Action <- getAction
  Argument <- getArgument
  Result <- Action Argument
  Config <- getConfig
  process Result Config

It’s tempting to get too clever about this, allowing bindings to refer to one another, traversing things in different orders, &c. Don’t!

Expression statements and the right-hand sides of let and monadic bindings are all desugared.

do
  f (<- x)
  let g = h (<- y)
  i <- (<- z)
  return i
-- ===
do
  A <- x
  f A

  B <- y
  let g = h B

  C <- z
  i <- C

  return i

Note that the x <- y syntax is now equivalent to let x = <- y; x is equivalent to let _ = <- x.

Records within do blocks are desugared, too.

do
  return R { m <- x, n = f (<- y) }
-- ===
do
  A <- x
  B <- y
  return R { m = A, n = f B }

This skirts problems of legibility with the RecordWildCards solution of:

do
  m <- x
  n <- f <$> y
  return R{..}

It’s also far less ambiguous than similar proposals, such as idiom brackets.

The left arrow would be treated as a level-10 precedence expression, like \, let, if, case, and do. Otherwise, inline-bind expressions would be ambiguous with ordinary bind statements:

do
  f <- x
  ...

To use an inline bind, this must be written as:

do
  f (<- x)
  ...
-- ===
do
  f $ <- x
  ...
@UnkindPartition
Copy link

This is beautiful.

@Heimdell
Copy link

Heimdell commented Apr 8, 2016

For me, it's unreadable. And it violates "retrival and usage separation" principe.

@fizruk
Copy link

fizruk commented Apr 8, 2016

Idris has so called !-notation which is very similar to this.

@ethercrow
Copy link

This example is really convincing

do
  f (<- a) b (<- c) d
-- ===
join $ f <$> a <*> pure b <$> c <*> pure d

@deepfire
Copy link

How much work would be turning this into a GHC proposal?

@agocorona
Copy link

We definitively need something like this

@mberndt123
Copy link

❤️

@evincarofautumn
Copy link
Author

I proposed this in 2017: ghc-proposals/ghc-proposals#64

The consensus seemed to be that the next step should be to prototype an implementation to evaluate, but I haven’t been able to do so. However, I think there’s not actually very much work left, because this can reuse a lot of the machinery already in place for ApplicativeDo.

@JakobBruenker
Copy link

FYI I implemented a plugin similar to this but using Idris's !-notation: https://www.reddit.com/r/haskell/comments/106opzn/ann_monadic_bang_a_plugin_for_more_concise/?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment