arliss_obsidian/fp/Language/Infix Operators.md
2024-09-22 14:24:51 -05:00

2.6 KiB

Infix operators are symbols that alias binary (2-argument) Functions.

They're defined like so:

infixl <precedence> <fn> as <operator>
-- or
infixr -- ..

e.g.

eq :: Int -> Int -> Boolean
eq = -- ...

add :: Int -> Int -> Int
add = -- ...

infixl 1 add as +
infixl 1 eq as ==

(1 + 2) == 3
-- same as
eq (add 1 2) 3

Associativity

Operators are either left or right associative (infixl or infixr).

When multiple infix operators with the same precedence are chained, associativity tells the language how to group them, e.g. && is right-associative, while * is left associative.

e.g.


a && b && c
-- interpreted as 
(a && (b && c))

a * b * c
-- interpreted as
((a * b) * c)

Precedence

The precedence of operators is an int from 1-9 used as a tie-break, for example in

a + b == c

this behaves how you'd expect; this is equivalent to

((a + b) == c)

as the precedence of + (6) is higher than =='s (4), the grouping is first done around a + b.

Common Operators

Operator Associativity Precedence Aliases
$

Directionality

Most commonly used operators have flipped variants, e.g.

  • function composition has g <<< f or f >>> g
  • function application has f $ a or a # f
  • Functor has f <$> a or a <#> f
  • Bind has f =<< m or m >>= f

In general, right-to-left operators tend to be easier to refactor into & out of because they closely mirror the expressions they replace:

map (add 1) maybeNum
-- into
add 1 <$> maybeNum

foo (bar a)
-- into
foo <<< bar

Left-to-right, on the other hand, can read better to humans and plays better with pipelines containing both [[Bind|bind >>=]] and [[Functor|map <#>]].

Consider an expression piping Maybe String to split :: String -> Array String then Data.Array.NonEmpty.fromArray :: Array a -> Maybe (NonEmptyArray a), then toLower each element:

String.toLower
  <$> (
    Array.NonEmpty.fromArray
    =<< String.split "."
    <$> mStr
  )

We need to wrap the right hand of the last map because the precedence of =<< is 1 (the lowest) and the precedence of <$> is 4.

Written RTL, though, gives:

mStr
  <#> String.split "."
  >>= Array.NonEmpty.fromArray
  <#> String.toLower

This works because <#>'s precedence (1) is the same as >>=. The lower precedence on flipped map means you'll often need more parentheses wrapping its arguments (..) <#> (..) >>= (..) as opposed to entire expressions .. <$> (.. =<< ..).

Personally, I try to stick to RTL except for expressions including bind.