Erlang encourages poor Functional Design

Thesis

While Erlang may be a functional programming language, it encourages poor functional design and ingrains the habit of excessive coupling, which I’ve witnessed programmers take with them to other languages.

Definitions

Loose coupling is one of the programming principals:

in which each of its components has, or makes use of, little or no knowledge of the definitions of other separate components.[1]

Functional design helps create loose coupling, it is:

A design method in which the system is seen from the functional viewpoint. The design concentrates on isolating high-level functions that can then be decomposed into and synthesized from lower-level functions. Development proceeds as a series of stepwise refinements of functionality.[2]

Such designs have a number of advantages: it’s easier understand and reason about small, clearly defined functions; it’s easier to test such functions; it increases code reuse; it decreases the blast radius when changes need to be made. It’s the first of the unix philosophiesmake each program do one thing well. It’s also the S in SOLID: single responsibility principle a class should have only a single responsibility.

Error handling by returned values

Functional programming languages tend to deal with errors by either throwing an exception or by returning an error value. Many functional languages—including erlang—support both. Regarding this thesis, the returned errors are the problematic ones in erlang.

The basic convention for returned errors in erlang is to return either the tuple {:ok, Value} for a successful computation or {:error, Details} for a failed one. This is just a convention, sometimes unpure things just return :ok, and sometimes failures return {:error, Reason, Details} or something altogether different.

A similar mechanism in Haskell is the Either type, which can either be Right(value) or Left(error), or the similar Try in Scala which can either be Success(value) or Failure(throwable).

Composition of fallible functions

The nice part about Haskell’s Either and Scala’s Try, are that they support functional composition on their failure types which—much like exceptions—will avoid continued computation on failure.

To show what this looks like, let’s use an example of a hypothetical session service, which takes a json http post with a username and password, decodes it, looks up the user details from the database, checks the password, and generates a jwt token to respond with.

In scala, our top-level code might look like the following:

@Json case class LoginRequest(username: String, password: String)

def login(dbConn: DbConnection, req: Request): Try[Response] =
  for {
    reqJsonStr <- req.body
    loginReq <- json.decode[LoginRequest](reqJsonStr)
    userDetails <- db.findUser(dbConn, loginReq.username)
    _ <- checkPassword(userDetails.hashedPassword, loginReq.password)
    jwtToken <- jwt.generateToken()
  } yield Response(jwtToken)

Each of the above methods returns a Try type and the for block conceptually desugars to something like:

def login(dbConn: DbConnection, req Reuqest): Try[Response] =
  req.body match {
    case Failure(err) =>
      Failure(err)
    case Success(reqJsonStr) =>
      json.decode[LoginRequest](reqJsonStr) match {
        case Failure(err) =>
          Failure(err)
        case Success(loginReq) =>
          db.findUser(dbConn, loginReq.username) match {
            ...
          }
      }
    }
  }

So if findUser() can’t find the user in the database, it will return an appropriate Failure(), which will be immediately returned and checkPassword() and the rest of the function won’t be executed. While there is a learning curve to writing code this way, it’s pretty obvious without knowing all the details that the result is fairly concise and readable.

What happens if we try writing the same thing in erlang? Unfortunately erlang doesn’t have built-in monadic blocks, so we end up needing to write code like the following:

validate(_DbConn, req#{body = <<>>}) ->
  {:error, "No body provided"};
validate(DbConn, req#{body = ReqJsonStr}) ->
  case json:decode(ReqJsonStr) of
    {:error, Reason} -> {:error, Reason};
    {:ok, #login_request{username = Username, password = Password}} ->
      case db.find_user(DbConn, Username) of
        {:error, Reason} -> {:error, Reason};
        {:ok, #user_details{hashed_password = HashedPassword}} ->
          ...
      end
  end.

As you can imagine, this nesting can get quite deep and it can quickly become really hard to read, so the urge—and typical next step—is to split this into multiple functions:

validate(_DbConn, req#{body = <<>>}) ->
  {:error, "No body provided"};
validate(DbConn, req#{body = ReqJsonStr}) ->
  decode_json(DbConn, ReqJsonStr).

decode_json(DbConn, ReqJsonStr) ->
  case json:decode(ReqJsonStr) of
    {:error, Reason} ->  {:error, Reason};
    {:ok, #login_request{username = ReqUsername, password = ReqPassword}}) ->
      find_user(DbConn, ReqUsername, ReqPassword)
  end.

find_user(DbConn, ReqUsername, ReqPassword) ->
  case db.find_user(DbConn, ReqUsername, ReqPassword) of
    {:error, Reason} -> {:error, Reason};
    {:ok, #user_details{hashed_password = HashedPassword}} ->
      check_password(ReqUsername, ReqPassword, HashedPassword)
  end.

...

At this point we’ve thrown functional design out the window. We are now passing a database connection to a “function” called decode_json() which doesn’t need a database connection. decode_json() is now directly calling find_user() which makes no sense because finding a user really has nothing to do with decoding json. We’re passing a password field around to code that really has no business having a raw password. We have all the coupling problems: code that’s harder to reuse, harder to read, harder to test, harder to refactor.

Unfortunately there’s not a lot we can do about it. To keep clean functions we’d want to have the caller handle the error, but that’s back to exactly the first version we were trying to refactor.

Evidence

To support that this is not just a hypothetical problem, here’s a few examples taken from projects listed at or near the top of trending erlang projects. This is not exhaustive. It doesn’t represent a lot of erlang code, which uses exceptions to side-step the problem, but it also doesn’t take very much hunting to find code like this:

disco_web.erl

validate_payload is a very simple example. One could argue that it could be fixed just by renaming it to run_if_payload_valid, but its still coupling, even if very vanilla coupling. There is no good way to reduce this coupling without making the call sites more complex (or using macros).

ejabberd_auth_sql.erl

This is an example of all the logic in a single function, this doesn’t have the coupling problem, but is getting hard to read, and is not doing nearly as much as business logic often has to do. Note how it’s already getting hard to see what the “false” on line 100 is referring to.

At line 119 there’s a different variation that has almost exactly the same logic except for the furthest nested guts. While there isn’t a coupling problem here, the avoidance of such has caused extensive duplication of code which is also problematic.

boss_web_controller.erl

Here execute_action_check_for_circular_redirect is being passed either arguments that it doesn’t care about. It’s not even actually doing a “check”, instead the check is being forced into the caller, it’s just pattern matching on true or false result of the check. The check is strongly coupled to the execute_action_inner, so there’s no easy way to test the circular redirection logic in isolation.

What does this mean?

Does this mean i should not use erlang?

Not at all! It just has weaknesses; every language has weaknesses. This particular weakness should be kept in mind when switching between languages to avoid taking unfortunate habits to other languages unnecessarily. Things like erlando or Monad for elixir can help in avoiding the problem; unfortunately with the dynamic type-system and existing ad-hoc use of error return values in existing libraries, monadic frameworks will never be as nice as working with similar types in haskell or scala, and these libraries may not be allowed in projects which aim to avoid dependencies.

Exceptions can also be used, with the downsides that they bring.

While I personally would not rule out using erlang, I also wouldn’t recommend it to someone as a language to learn functional programming in, as I fear it will encourage the formation of subideal coding inclinations.

Strengthened thesis

While code written in this style in erlang are technically still functions—mappings to a single output for any given input—I feel we’ve lost the essence of functional programming; creating equations that individually represent truths and composing those in larger and larger pieces into programs. Instead we’re telling the computer to do A and then do B and then do C: imperative programming.

References

  1.  wikipedia: loose coupling
  2. functional design.A Dictionary of Computing. . Encyclopedia.com. 11 Oct. 2016

~ by pulotka on 2016/10/17.

3 Responses to “Erlang encourages poor Functional Design”

  1. Seems you missed the point of Erlang’s “die quickly”…

    It’s usually used like this:
    {ok, Result1} = fun1(Args),
    {ok, Result2} = fun2(Result1),

    Failure (mismatch) is catched by a catch keyword or (preferably) by OTP subsystem.

    • I completely agree, if you use exceptions in Erlang you completely avoid this problem.

  2. I think the attempted refactor here is half-hearted and could have been better. It’s always possible to write poor code in any language, and presented refactor doesn’t look like idiomatic erlang either.

Leave a comment