Yan Yi

TIL: Gleam Pattern Matching and Pipe

· Yan Yi Goh· 3 min read

It has been slightly more than two years since I have written any Elm. Plus with the advancement of AI-assisted coding, I have not really written much code by hand over the past year.

I decided to give Exercism a try. In particular, I picked up the Gleam track. I wanted to note down these two exercises which I just did. As someone who did Elm before, I felt super rusty not knowing how to implement but did them in a noob way.

Guessing Game

This exercise teaches pattern matching. Implement the function that will, given an integer, return:

  • 42? “Correct”
  • 41 or 43? “So close”
  • Less than 41? “Too low”
  • More than 43? “Too high”

My initial implementation worked:

pub fn reply(guess: Int) -> String {
  case guess {
    42 -> "Correct"
    41 | 43 -> "So close"
    i if i < 41 -> "Too low"
    i if i > 43 -> "Too high"
    _ -> "Too high"
  }
}

But it left me pondering: why do I need to _ -> "Too high" just to satisfy the compiler? In Elm, usually I would avoid using _ -> for pattern matching because rarely I will pattern match on integers or strings. In this exercise, however, we are working with an integer. Without the _ ->, the compiler for Gleam complains:

error: Inexhaustive patterns
  ┌─ src/guessing_game.gleam:2:3
2 │ ╭   case guess {
3 │ │     42 -> "Correct"
4 │ │     41 | 43 -> "So close"
5 │ │     i if i < 41 -> "Too low"
6 │ │     i if i > 43 -> "Too high"
7 │ │   }
  │ ╰───^
This case expression does not have a pattern for all possible values. If it
is run on one of the values without a pattern then it will crash.
The missing patterns are:
    _

Then, I went with something like:

pub fn reply(guess: Int) -> String {
  case guess {
    42 -> "Correct"
    i if i < 41 -> "Too low"
    i if i > 43 -> "Too high"
    _ -> "So close"
  }
}

Slightly better.

Log Levels

This exercise was to implement three functions which will print out parts of a given log line or reformat the log line. However, for brevity, I will stick to just one function. For example, given "[ERROR]: This is an error", either:

  1. Return just the error message: "This is an error"
  2. Return the the log level: "error"
  3. Or reformat the log: "This is an error (error)"

My initial implementation was:

pub fn message(log_line: String) -> String {
  case log_line {
    "[INFO]: " <> msg -> string.trim(msg)
    "[WARNING]: " <> msg -> string.trim(msg)
    "[ERROR]: " <> msg -> string.trim(msg)
    _ -> "Unknown"
  }
}

The repetitive string.trim() annoyed me a little. Then I remembered that since these can be pure functions, whatever is the last line will be returned. So, I can do something like this with a pipe:

pub fn message(log_line: String) -> String {
  case log_line {
    "[INFO]: " <> msg -> msg
    "[WARNING]: " <> msg -> msg
    "[ERROR]: " <> msg -> msg
    _ -> "Unknown"
  }
  |> string.trim
}

Slightly better. This will read as “pattern match, take the result, pipe it to a string.trim function”.