In this article, we’ll examine some interesting similarities and differences between the F# and Elixir programming languages. Keep in mind that this comparison is not exhaustive; we’ll just cover a few points that are particularly noteworthy.
Comparison Operators
F# and Elixir have distinct ways of comparing values. F# uses a single ‘=’ for equality comparison, while Elixir uses two comparison operators: ‘==’ and ‘===’. The former is a standard comparison operator, while the latter is primarily used for comparing numbers of the same type. Here’s an example:
// F#
1 = 2 // false
1 = 1 // true
1 = 1.0 // This yields compile error
float(1) = 1.0 // true
# Elixir
1 == 2 # false
1 == 1 # true
1 == 1.0 # true
1 === 1.0 # false
F# is a strongly typed language, so it does not allow comparisons between values of different types. Elixir, on the other hand, is dynamically typed and performs type casts during runtime.
Inequality comparisons in F# use ‘<>’, while Elixir uses ‘!=’ and ‘!==’. Elixir’s syntax aligns more closely with most programming languages, but the type safety of F# is also a strong point. Additionally, these languages use different conventions for comments.
Elixir also employs the ‘=’ operator for pattern matching, which is quite powerful.
Immutability in Elixir and F#
Both languages are immutable by default, but their approaches differ. In Elixir, values are immutable, but “labels” can be reassigned to other values:
# Elixir
a = 1 # value "1" is now labelled "a"
a = a+1 # label "a" is changed: now "2" is labelled "a"
a = a*5 # value "10" is now labelled "a"
To refer to the current value of a variable when using the match operator in Elixir, you can use the following syntax:
# Elixir
b = 1
b = 2 # rebinding variable to 2
^b = 3 # matching: 2 = 3 -> error
In F#, mutability is allowed but must be explicitly declared. Mutability is mainly used for compatibility with .NET libraries, so it should not be overused:
// F#
let a = 1 // binding value "1" to label "a"
a = 2 // returns false (it is just comparing)
a <- 2 // compile error
let mutable b = 1 // binding value "1" to mutable variable "b"
b <- 2 // changing value of variable "b"
b = 2 // returns true
F# is the clear winner in this aspect, as it prohibits changing values bound to a label, resulting in more readable code.
List Operations
Both languages have similar list operations. One unique feature in Elixir is the ability to match not only the head and tail of a list but also several leading elements:
# Elixir
[ a, b, c | tail ]
// F#
head::tail
a::b::c::tail // that also works
The pipe operator (‘|>’) functions similarly in both languages, though it binds to the first parameter of the function in Elixir and the last in F#. The primary difference here is a matter of convention and does not significantly impact the overall functionality. However, it is important to understand how it affects currying and partial application in both languages, as discussed in the comments section.
Elixir does not have a “for” loop like F#, which means list iteration must be done using a functional approach, such as recursion. This can be seen as an advantage for Elixir because it encourages the use of a functional programming style, whereas F# allows for loops, which may lead to imperative programming habits.
Functions and Modules
One aspect of Elixir that may be considered cumbersome is the requirement to pair each ‘def’ and ‘defp’ with an ‘end’. This can make the code look cluttered and is reminiscent of curly braces or Visual Basic. In F#, code blocks are delimited by whitespace levels, similar to Python.
Elixir also requires functions to be wrapped in modules. While this may not be a significant inconvenience, it is an extra step not required in F#. On the other hand, Elixir allows for multilevel modules, which can be useful in certain cases.
Pattern Matching in F# and Elixir
Elixir’s pattern matching capabilities were briefly mentioned earlier, and its “=” operator has impressive qualities. Pattern matching can be done on function parameters, as shown in the second example below, and can be further simplified with guards:
# Elixir
# case statement
def blank?(value) do
case value do
nil -> true
false -> true
"" -> true
_other -> false
end
end
# pattern matching on function parameters
def blank?(nil), do: true
def blank?(false), do: true
def blank?(""), do: true
def blank?(_other), do: false
# pattern matching on function parameters with guards
def blank?(value) when value in [nil, false, ""], do: true
def blank?(_other), do: false
In F#, pattern matching is similar to Elixir’s case statement, with the option to use guards and more:
// F#
let x =
match 1 with
| 1 -> "a"
| 2 -> "b"
| _ -> "z"
Due to F#’s strong typing, replicating the same example is not straightforward. Discriminated unions can be used to achieve similar functionality in F#. In this case, the author prefers F#’s approach to pattern matching.
While both F# and Elixir have their merits, the author’s personal preference is F#. Having used F# for five years, the author finds it to be the more elegant language. Elixir, however, holds a strong second place in terms of beauty. It is commendable that Elixir emphasises functional programming, though there are features in F# (such as discriminated unions and units of measure) that do not have clear equivalents in Elixir. The strong typing in F# also remains a significant advantage.