ocaml

Using the OCaml reverse application operator |> to construct a function without wrapping it in a `fun ->` declaration


I often find myself using the |> operator when constructing functions to pass as parameters to methods like List.find. For example, I have this code snippet:

let parse_test_header (header : string list) : string * (string list) =
  let is_attr (line : string) : bool = ':' == get line 0 in
  let test_name = List.find (fun line -> line |> is_attr |> not) header in
  let test_attrs = List.filter is_attr header in
  (test_name, test_attrs)

For simplicity I would like to use |> without having to wrap it in a fun ... -> ... first, something like:

let parse_test_header (header : string list) : string * (string list) =
  let is_attr (line : string) : bool = ':' == get line 0 in
  let test_name = List.find (is_attr |> not) header in
  let test_attrs = List.filter is_attr header in
  (test_name, test_attrs)

However, this gives

31 |   let test_name = List.find (is_attr |> not) header in
                                  ^^^^^^^
Error: This expression has type string -> bool
       but an expression was expected of type bool

Is there some way to accomplish this?


Solution

  • "Reverse application" vs. composition

    Consider how |> is implemented:

    let ( |> ) x f = f x
    

    You're passing is_attr as the first argument, which is a function, which means not would need to take a function as its argument. It doesn't so you get a type mismatch.

    You're looking for function composition which can be tricky when used to compose polymorphic functions due to the value restriction, but can be implemented. Let's call this operator |..

    let ( |. ) f g x = g (f x)
    

    Now:

    let parse_test_header (header : string list) : string * (string list) =
      let is_attr (line : string) : bool = ':' == get line 0 in
      let test_name = List.find (is_attr |. not) header in
      let test_attrs = List.filter is_attr header in
      (test_name, test_attrs)
    

    Note that in your is_attr function you're using physical equality (==) rather than structural equality (=). This may not always do what you expect.

    Making composition unnecessary

    You're probably also looking for List.partition. The composition of is_attr and not is no longer necessary if we bind ttest_name to the second list resulting from the call List.partition is_attr header indicating results which are not attributes.

    let parse_test_header (header : string list) : string * (string list) =
      let is_attr (line : string) : bool = ':' = get line 0 in
      let (test_attrs, test_name) = List.partition is_attr header in
      (List.hd test_name, test_attrs)
    

    Additionally, is_attr can be defined in terms of String.starts_with.

    let parse_test_header (header : string list) : string * (string list) =
      let is_attr = String.starts_with ~prefix: ":" in
      let (test_attrs, test_name) = List.partition is_attr header in
      (List.hd test_name, test_attrs)
    

    Associativity

    Now, |. associates left to right. This means we can compose similarly to the way |> works.

    fun x -> x |> f |> g |> h
    

    Is equivalent to:

    f |. g |. h
    

    If you wished to emulate Haskell's . operator for composition, you'd want right-to-left associativity. Associativity of OCaml operators is governed by the first character in the operator.

    fun x -> f @@ g @@ h x
    

    Is equivalent to:

    f @. g @. h
    

    Where @. is defined:

    let ( @. ) f g x = f (g x)