Bind

fble-0.5 (2025-07-13,fble-0.4-212-ga8f8ad0f)

This tutorial describes the function bind syntactic sugar in fble and the motivations for it.

The Syntax

Bind is syntactic sugar in fble to make the syntax a little bit nicer in the following very specific situation:

We'll talk later about why this case occurs. For now, let's just look at the syntax.

Definition

Bind syntax takes something of the form:

T1@ a1, T2@ a2, ... <- FUNC;
BODY;

And desugars it to:

FUNC((T1@ a1, T2@ a2, ...) { BODY; });

Where FUNC and BODY are fble expressions and BODY can refer to the arguments a1, a2, .... Bind syntax can be used with one or more arguments a1, a2, ....

Example

In the Functions tutorial we gave an example of a function that takes a function as an argument:

((Bit@, Bit@) { Bit@; }, Bit4@, Bit4@) { Bit4@; }
BinaryBitwise = ((Bit@, Bit@) { Bit@; } op, Bit4@ a, Bit4@ b) {
  Bit4@(op(a.3, b.3), op(a.2, b.2), op(a.1, b.1), op(a.0, b.0));
};

And we gave an example of passing an anonymous function to that:

(Bit4@, Bit4@) { Bit4@; } And4 = BinaryBitwise(
  (Bit@ a, Bit@ b) {
    a.?(0: 0, 1: b);
  }
);

Let's rewrite the BinaryBitwise function to something where we could apply the bind syntax. We need it to take a single function argument, so we'll swap the order of arguments and have it return a function that takes the single function argument:

(Bit4@, Bit4@) { ((Bit@, Bit@) { Bit@; }) { Bit4@; }; }
BinaryBitwise = (Bit4@ a, Bit4@ b)((Bit@, Bit@) { Bit@; } op) {
  Bit4@(op(a.3, b.3), op(a.2, b.2), op(a.1, b.1), op(a.0, b.0));
};

We can define And4 using our new BinaryBitwise function as follows:

(Bit4@, Bit4@) { Bit4@; } And4 = (Bit4@ x, Bit4@ y) {
  BinaryBitwise(x, y)(
    (Bit@ a, Bit@ b) {
      a.?(0: 0, 1: b);
    });
};

Now all the requirements for bind are satisfied when we call the BinaryBitwise(x, y) function: it takes a single function argument which we are supplying with an anonymous function.

Using the bind syntax, we can rewrite the code to:

(Bit4@, Bit4@) { Bit4@; } And4 = (Bit4@ x, Bit4@ y) {
  Bit@ a, Bit@ b <- BinaryBitwise(x, y);
  a.?(0: 0, 1: b);
};

You can double check that if we apply the definition for bind syntax from above to this code, you'll get the right thing back.

A Less Contrived Example

The example we gave for bind syntax in the previous section was totally contrived, just to demonstrate the syntax. You wouldn't implement And4 using bind syntax in practice.

Let's take a look at a less contrived example. Imagine you are doing arithmetic with integers. Division by zero is undefined for integer arithmetic. One way to deal with division by zero is to change the return type of division to distinguish between an integer result and an undefined result.

For example, let's define a type MaybeInt@ that holds either an integer or nothing:

@ MaybeInt@ = +(Int@ just, Unit@ nothing);

(Int@) { MaybeInt@; } Just = (Int@ x) {
  MaybeInt@(just: x);
};

MaybeInt@ Nothing = MaybeInt@(nothing: Unit);

Now we can define a Div function for integer division with the following type:

(Int@, Int@) { MaybeInt@; } Div = (Int@ a, Int@ b) { ... };

The Div function will return Nothing if it is a divide by zero and Just(x) otherwise.

Any time you work with this division function, you'll have to check the answer to see if it is undefined. To propagate that case, it would be nice if we have variations of all our arithmetic operations to work with MaybeInt@ instead of Int@. For example:

(MaybeInt@, MaybeInt@) { MaybeInt@; }
MaybeAdd = (MaybeInt@ ma, MaybeInt@ mb) {
  ma.?(nothing: Nothing);
  mb.?(nothing: Nothing);
  Just(Add(ma.just, mb.just));
};

(MaybeInt@, MaybeInt@) { MaybeInt@; }
MaybeDiv = (MaybeInt@ ma, MaybeInt@ mb) {
  ma.?(nothing: Nothing);
  mb.?(nothing: Nothing);
  Div(ma.just, mb.just);
};

We can factor out glue code for checking if a MaybeInt@ value is Nothing:

(MaybeInt@, (Int@) { MaybeInt@; }) { MaybeInt@; }
Maybe = (MaybeInt@ mx, (Int@) { MaybeInt@ } f) {
  mx.?(nothing: Nothing);
  f(mx.just);
};

Now we can write:

(MaybeInt@, MaybeInt@) { MaybeInt@; }
MaybeAdd = (MaybeInt@ ma, MaybeInt@ mb) {
  Maybe(ma, (Int@ a) {
    Maybe(mb, (Int@ b) {
      Just(Add(a, b));
    });
  });
};
  
(MaybeInt@, MaybeInt@) { MaybeInt@; }
MaybeDiv = (MaybeInt@ ma, MaybeInt@ mb) {
  Maybe(ma, (Int@ a) {
    Maybe(mb, (Int@ b) {
      Div(a, b);
    });
  });
};

Now we have code where it makes sense to use the bind syntax:

(MaybeInt@, MaybeInt@) { MaybeInt@; }
MaybeAdd = (MaybeInt@ ma, MaybeInt@ mb) {
  Int@ a <- Maybe(ma);
  Int@ b <- Maybe(mb);
  Just(Add(a, b));
};
  
(MaybeInt@, MaybeInt@) { MaybeInt@; }
MaybeDiv = (MaybeInt@ ma, MaybeInt@ mb) {
  Int@ a <- Maybe(ma);
  Int@ b <- Maybe(mb);
  Div(a, b);
};

I would argue this is slightly better than our original definitions for MaybeAdd and MaybeDiv because we've factored out the logic for testing if a value is undefined or not. If we wanted to, we could easily change the definition of MaybeInt@ to take an error message in the case of undefined and propagate that without having to change the definitions of MaybeAdd or MaybeDiv.

Type Inference

Bind is syntactic sugar for function application, which means it supports type inference.

Let's generalize our MaybeInt@ type to work for any kind of type using polymorphism:

<@>@ Maybe@ = <@ T@> { +(T@ just, Unit@ nothing); };

<@ T@>(T@) { Maybe@<T@>; } Just = (T@ x) {
  Maybe@<T@>(just: x);
};

Maybe@ Nothing = <@ T@> { Maybe@<T@>(nothing: Unit); };

Now we could propagate a divide by zero error to a boolean comparison result, for example. We'll start by updating our definition of the Maybe function for the case where the argument type and result types can be different:

<@ A@>(Maybe@<A@>)<@ B@>((A@) { Maybe@<B@>; }) { Maybe@<B@>; }
Maybe = <@ A@>(Maybe@<A@> mx)<@ B@>((A@) { Maybe@<B@> } f) {
  mx.?(nothing: Nothing<B@>);
  f(mx.just);
};

Take a moment to understand the interleaving of type parameters A@ and B@ and functions in that definition.

Now let's write a MaybeEquals function to test whether two Maybe@<Int@> values are equal. First without the bind syntax:

(Maybe@<Int@>, Maybe@<Int@>) { Maybe@<Bool@>; }
MaybeEquals = (Maybe@<Int@> a, Maybe@<Int@> b) {
  Maybe<Int@>(ma)<Bool@>((Int@ a) {
    Maybe<Int@>(mb)<Bool@>((Int@ b) {
      Just<Bool@>(Equals(a, b));
    });
  });
};

With bind syntax:

(Maybe@<Int@>, Maybe@<Int@>) { Maybe@<Bool@>; }
MaybeEquals = (Maybe@<Int@> a, Maybe@<Int@> b) {
  Int@ a <- Maybe<Int@>(ma)<Bool@>;
  Int@ b <- Maybe<Int@>(mb)<Bool@>;
  Just<Bool@>(Equals(a, b));
};

The problem with this is we have to repeat the Bool@ type argument every time we call the Maybe function. If we changed the result type to Maybe@<Int@> instead of Maybe@<Bool@>, we would have to update every line where Bool@ is mentioned.

Fortunately we can take advantage of type inference here. In fact, this scenario where we chain together bind syntax is the true motivation for having type inference in fble:

(Maybe@<Int@>, Maybe@<Int@>) { Maybe@<Bool@>; }
MaybeEquals = (Maybe@<Int@> a, Maybe@<Int@> b) {
  Int@ a <- Maybe(ma);
  Int@ b <- Maybe(mb);
  Just(Equals(a, b));
};

Monadic Computations

This idea of factoring out common glue logic for a data type into separate functions is very powerful and can be applied to many more things than the example we showed with the Maybe@ type. This is related to the term "Monad" from category theory, so we call this kind of thing "Monadic Computation".

The relevant pieces for monadic computation in the Maybe@ example were:

We can define an interface that captures these relevant pieces. We'll have the interface apply to an abstract type M@ in place of Maybe@, rename the Just function to return, and rename the Maybe function to do:

<<@>@>@ Monad@ = <<@>@ M@> {
  *(
    <@ A@>(A@) { M@<A@>; } return,
    <@ A@>(M@<A@>)<@ B@>((A@) { M@<B@>; }) { M@<B@>; } do
  );
};

An example instance of this Monad@ interface is what we've already seen:

Monad@<Maybe@> MaybeMonad = @(return: Just, do: Maybe);

We can generalize our arithmetic operations to work on any data type that supports this Monad@ interface. In the Maybe@ example we had the Nothing function to use in the case of MaybeDiv. Nothing is like a primitive function of the Maybe@ monad.

Here's how we could define monadic add, equality, and division functions for an abstract monadic type M@ with a given primitive function for the case of divide by zero:

<<@>@ M@>(Monad@<M@> m, <@ A@> { M@<A@>; } divide_by_zero) {
  (M@<Int@>, M@<Int@>) { M@<Int@>; } AddM = (M@<Int@> ma, M@<Int@> mb) {
    Int@ a <- m.do(ma);
    Int@ b <- m.do(mb);
    m.return(Add(a, b));
  };

  (M@<Int@>, M@<Int@>) { M@<Int@>; } DivM = (M@<Int@> a, M@<Int@> b) {
    Int@ a <- m.do(ma);
    Int@ b <- m.do(mb);
    b.?(0: divide_by_zero<Int@>);
    m.return(SaveDiv(a, b));
  };

  (M@<Int@>, M@<Int@>) { M@<Bool@>; } EqualsM = (M@<Int@> a, M@<Int@> b) {
    Int@ a <- m.do(ma);
    Int@ b <- m.do(mb);
    m.return(Equals(a, b));
  };

  @(AddM, DivM, EqualsM);
};

To get definitions for MaybeAdd, MaybeDiv, and MaybeEquals, we just need to pass Maybe@, MaybeMonad, and Nothing to the above function.

Here are some different things you could do with monads:

This is how standard input and output is described in fble. More about that in a later tutorial.

Bind in the Language Specification

See section 7.3 of the fble language specification for the specification of the bind syntax.

Next Steps

Head over to the Private Types tutorial to learn about private types in fble.