Are "do" blocks imperative?

do blocks let the programmer specify some ordered steps, so do they make Elixir kind of imperative or do they work like do blocks in Haskell that are just syntactic sugar for a chain of >>= operations?

1 Like

Elixir is not a lazy language. The steps written down in functions, macros, and other sequences of multiple lines of code will be executed from top to bottom.

This is something that is orthogonal to the concept of imperative vs functional: The important difference being that in Elixir youā€™re always passing data around (everything is an expression) and the data structures that you look at are immutable (rather, youā€™re always creating new versions of the data structures). In an imperative language, youā€™d be mutating your data structures all the time.

So: No, do-blocks are not ā€˜imperativeā€™. They just execute a sequence of (functional) steps in-order from top-to-bottom.

There are libraries that contain macros to turn code written as a do-block into a Monad, if you want (and require) that.

5 Likes

My understanding is that do blocks are imperative, because each line runs one after another, even if the return value is not used.

Itā€™s not enough to say that this is because the language is strict (as opposed to lazy) because there are non-imperative strict languages that will re-order statements and possibly remove those for which the return value is not consumed. Elm is an example of such a language.

Elixir is both strict and imperative.

2 Likes

You are correct that there are other languages that might re-order statements depending on if the return value is used. However, this has to do with purity/impurity rather than functional vs imperative.

Elm is a (strict) pure functional language. Elixir is a (strict) impure functional language. Another common example of an impure functional language would be the various Lisp dialects.

Imperative programming really is defined as ā€˜programming with statements that mutate the programā€™s stateā€™, which says nothing about ā€˜running lines after one-anotherā€™.

2 Likes

This is exactly what we do in Elixir and Erlang, each statement may have side effects that alter the programā€™s state.

Languages that allow you to do this must not change the order in which statements are executed as it would change the result of the program. The two are the same.

The alternative to being imperative is to explicitly express an ordering between two side effecting functions, which is what Haskell does with >>=. Elixir has no such construct.

I think we are mixing up the concerns? do is not necessarily about side-effects, not even in Haskell:

getRight :: Either a b -> Maybe b
getRight y =
   do Right x <- y
      return x

What you describe is all about sequence and guaranteeing the order of operations. Of course, if there are no side-effects, then the order of those operations likely do not matter and you are welcome to move things around. But you donā€™t have to shuffle either. That raises the question though: does being strict limit the amount of shuffling that can be done? :thinking:

In any case, it would be perfectly fine for a language to be sequential and without side-effects. But when you have side-effects, you typically want to guarantee the sequence of those side-effects too, so those are often seen together.

The other question, which is a bit more on the definition side of things, is if side-effects without a special notation is enough to warrant being labelled as imperative. Some would say thatā€™s a requisite for being functional. But typically, imperative is used to refer to programming languages that progress its own state via side-effects, a good example being while loops compared to recursion.

4 Likes

do is about sequencing in Haskell, and sequencing is required for useful side effects. Prior to having >>= (which do is syntactic sugar for) to explicitly specify dependencies between expressions Haskell didnā€™t have any way to perform side effects.

Thatā€™s a fun question! Iā€™m thinking it does not, but I imagine Iā€™ll be thinking about this one all afternoon :slight_smile:

Iā€™m aware that the literature does use ā€œstatementā€ in connection with Erlang/Elixir but ultimately that just leads to a sloppy mental model and sloppy thinking. Between do and end is a sequence of expressions.

And while the monadic approach to programming with actions as first class values is a hallmark of Haskell, it isnā€™t a requirement for functional programming in general.

Typically statements are the building blocks for imperatively structured logic while expressions are building blocks for functional logic for the transformation of values/data.

  • Place Oriented Programming (PLOP) - statements manipulate information in place. Statements focus on controlling program flow (imperative - change this, then change that).

  • Value Oriented Programming - expressions transform existing values into new ones. The expressions are sequenced to control the flow of values (functional - functions inherently consume their arguments to produce an new value).

Value of Values

Because Erlang and Elixir are impure it is possible to express an imperative style of programming but ultimately that goes against the grain of itā€™s functional nature.

1 Like

Another post as I accidentally pressed submit, oops.

I actually think itā€™s the inverse. An imperative language has the special notation for sequencing expressions (and thus side effects), a pure language leaves this to be implemented by the developer.

In a language with C style syntax sequencing is in the core language in the form of ;, which is an operator that says evaluate the left hand side, then the right hand side.

In a pure language a function body is a single expression and thus a user implemened ; needs to have the right hand side depend on the result of the left hand side in some fashion. We can do this by wrapping making the right hand side a function (or similar) that takes the value of the left hand side- then the right hand side can only be evaluated after the left hand side has been evaluated.

The type of ; would be fn(a, fn(a) -> b) -> b

1 Like

do is all, and only, about sequencing, it does not make anything imperative. Whether the ordering is important depends of course on what you do in the expressions inside a do, that is side-effects.

You have 2 different ā€œtypesā€ of side effects in erlang/elixir: messages and errors, both of which can make the order of evaluation important. Messages are pretty obvious but errors are much more subtle. So even if I have 2 sequences of expressions which do the same thing and guarantee the same order of messages they can have different behaviour if an error occurs.

A simple example. I have 3 operations I want apply to each element of a list, a(e), b(e) and output(e) where the return values of a and b are passed onto to the next operation. We assume here that a and b are side-effect free, i.e. pass no messages, while output has side-effects. One way to do this would be to use a pipe:

as |> Enum.map(fn(e) -> a(e) end)
   |> Enum.map(fn(e) -> b(e) end)
   |> Enum.map(fn(e) -> output(e) end)

while another way would be to map over the list once:

as |> Enum.map(fn(e) -> output(b(a(e))) end)

These are NOT equivalent if an errors occurs inside a call to either a or b. This is important because in erlang/elixir errors are a part of the language. The way they affect execution and how they can be handled is well defined so you have to always be aware of them and how affect your system. In this trivial case how some things may or may not occur, in this case output depending on ordering and errors.

Apart from this the code inside a process is pure (except for process dictionaries).

4 Likes