The language Scala promises a smooth migration path from Object-oriented Java to functional programming. It runs on the JVM, has concepts both from OOP and FP, and an emphasis on interoperability with Java.
As a multi-paradigm language, Scala can flatten the learning curve for those already familiar with OOP, at the price of making some compromises in language design that can make functional programming in Scala more cumbersome than it has to be.
If you like the functional aspects of Scala, but are somewhat bothered by the annoyances that doing FP in Scala incurs, you should take a look at Haskell! It is one of the languages that inspired Scala, so you will find many familiar concepts. By fully embracing the functional paradigm, Haskell makes functional programming much more convenient. The following is an (incomplete) overview of some of the things you can expect from Haskell.
If you want to try out Haskell right away, you can find instructions for getting started here. We have collected some useful training resources at FP Complete Haskell Syllabus, and also provide commercial training. If you’re planning to use Haskell for a project and want some help, we can provide you with Consulting services.
Scala uses a Java-like syntax, but avoids a lot of unnecessary
boilerplate by making things like an end-of-line semicolon or empty
code blocks { }
optional.
Haskell goes much further in terms of conciseness of syntax. For
instance, function application, the most common operation in a
functional language, is indicated by whitespace, writing f
x
instead of f(x)
.
As an example of Haskell’s conciseness, consider the definition
of a binary tree containing data of some type A
. In
Scala, you’d define a trait
, and a case
object
/case class
each for an empty node and a
node that contains an A
, and left and right sub-trees,
like this:
sealed trait Tree[+A]
case object Empty extends Tree[Nothing]
case class Node[A](v: A, l: Tree[A], r: Tree[A]) extends Tree[A]
The Haskell equivalent is
data Tree a = Empty | Node a (Tree a) (Tree a)
Another thing that is syntactically more pleasant in Haskell is currying: in Scala, if you want to use a curried function, you have to anticipate this when defining the function, and write
def curriedF(x: Int)(y: Int) = ...
instead of
def f(x: Int, y: Int) = ...
and then use an underscore to get a one-argument function, as in
curriedF(x)_
. In Haskell, you can curry any function
by applying it to a subset of its arguments; if you define a
function of two arguments, as in
f x y = ...
then f x
is a function of one argument.
Function composition, i.e., combining two functions f :: b
-> c
and g :: a -> b
to a function from
a
to c
, is simply indicated by a dot, as
in f . g :: a -> c
. With function composition and
currying, it is possible to combine small functions with minimal
visual overhead. For example,
tenSmallest = take 10 . sort
defines a function that will first sort a list, and then return the first 10 elements.
The minimalistic syntax of Haskell both avoids excessive typing, and makes it easier to read code, since there is less distracting boilerplate.
Both Scala and Haskell are statically typed languages, but Haskell’s type system is arguably more powerful, in the sense that it provides more guarantees that are checked by the compiler. It also has superior type inference, which means that it places a lower burden on the programmer.
One of the benefits of using a type system is that it prevents you from making mistakes such as passing an argument to a function that makes no sense. The more fine-grained the type system is, the more mistakes it can preclude.
For instance, you might have many values of type
Double
in your program, where some may stand for
lengths as measured in meters, some for times in seconds, etc. If
the type system only knows that those are Double
s, it
will not get in your way when you do a silly mistake, such as
adding a time and a distance.
In Scala, you can wrap a Double
up in a value
class that represents a time interval as in,
class Seconds(val value: Double) extends AnyVal
This type is essentially a Double
, but Scala will
not let you mix up values of type Seconds
and
Double
, eliminating a class of possible errors.
Scala tries to avoid instantiation of value classes, using the underlying type as the runtime representation whenever possible. However, since the JVM does not support values classes, under some circumstances (such as when performing pattern matches or storing a value class in an array), instantiation is unavoidable and there is a run-time cost to the abstraction.
If we want to perform operations, such as addition or
subtraction, on the new type Seconds
, we’ll have to
define operations on them, such as
class Seconds(val value: Double) extends AnyVal {
def +(x: Seconds): Seconds = new Seconds(value + x.value)
def -(x: Seconds): Seconds = new Seconds(value - x.value)
}
While this is straightforward, it is boilerplate code that you’d like to avoid writing yourself.
In Haskell, you can: defining Seconds
as a
newtype
, as in
newtype Seconds = Seconds Double deriving (Eq, Num, Ord)
will define a new type, Seconds
, that has numerical
operations, comparisons, and equality defined. Furthermore, the
run-time representation of this will always be a plain
Double
, so there is no cost for this abstraction.
Since newtypes
are so lightweight (both in terms of
performance and in the amount of boilerplate code), they are used
pervasively in Haskell projects, which contributes to clean, robust
code.
A prominent example is the IO
type: in Haskell, the
type signature
f :: a -> b
indicates a function f
that takes a value of type
a
, and returns a value of type b
, without
producing any side effects. This is distinct from
g :: a -> IO b
which says that g
takes input of type
a
, gives output of type b
, but also can
also perform some IO in between.
Just by looking at the type signature, a programmer can conclude
that f
does not interact with the file system, the
network, or the user. This means that the function f
is referentially transparent, i.e., an occurrence of f
in the code is equivalent to its result.
In Scala, writing referentially transparent functions is encouraged as a best practice, but it is not evident, without reading the actual code, whether a given function has side effects or not. In Haskell, referential transparency is enforced by the type system, which not only gives important information to the programmer, but also allows for optimizations in the form of rewrite rules.
As an example, consider the consecutive application of two
functions f :: a -> b
and g :: b ->
c
to all the elements of a list, via map
. You
can either write this as
map g . map f
or as
map (g . f)
For referentially transparent functions, both versions give the same result, but the first version involves two list traversals, and the second only one. Since referential transparency is evident in the types, GHC can safely use a rewrite rule to replace the first version with the second.
These rewrite rules are not hard-wired into the compiler, but
are a feature that is exposed to the programmer, so that when you
implement your own data structures, you can define your own rewrite
rules to boost efficiency. A prominent example is the
vector
package, which uses re-write rules to performs
stream fusion, giving c-like performance to high level code.
Scala has the notion of implicit conversions, that let
you add functionality to your types. For instance, declaring an
implicit conversion to the Ordered
trait lets you add
functions for comparing values with respect to some ordering.
Implicit conversions are a powerful tool, but also one that can
lead to subtle errors. Since implicits can be brought to scope
anywhere, you have no guarantee of consistency.
For example, when you use implicit conversions to store and retrieve data in a sorted set, there is no guarantee that the implicit conversion used for storage and retrieval is the same.
In Haskell, you would instead write an instance of the
Ord
typeclass for your data type. Since writing
multiple instances of a given type class for the same data type is
a compile-time error, consistency is guaranteed.
One argument brought against static type systems is that writing down type signatures for every function can be tedious. But modern languages like Scala and Haskell feature algorithms that let the compiler infer the type of a function when none is stated explicitly.
Scala, being a multi-paradigm language, has to account for inheritance, which makes type inference more challenging. As a result, in many cases Scala cannot infer types that are much easier to infer in Haskell.
If you miss features like existential or universal quantification, type families, or generalized algebraic data types, Haskell has those as well.
With many researchers amongst its users, Haskell gets new extensions to its type system regularly. One notable example is Liquid Haskell, which lets you encode restrictions to your types, and pre- and postconditions for your functions, and have their correctness proven at compile time, eliminating the need for run-time checks.
Functional programming relies heavily on the use of recursion where imperative languages would use loops. In order not to blow the stack, tail call optimization is employed.
Unfortunately, due to limitations in the JVM, Scala only has
fairly limited tail call optimization, in that it only optimizes
self-tail calls. This excludes the case of multiple functions
making tail calls to each other (which flatMap
does),
severely limiting the usefulness of functional programming in
Scala.
There are workarounds for this, such as using trampolines, but they come with performance costs. Thus, when programming in Scala, you often face the question whether it is ok to using functional programming techniques for some problem, or if the price you pay in terms of performance would be too high.
Haskell does not have this problem, since all tail calls are optimized.
A strong selling point of Scala is its interoperability with Java, allowing the re-use of a plethora of existing libraries.
Haskell provides a lot in terms of language interoperability as well:
With its origins in academia, Haskell gets a reputation of being a language purely for the Ivory Tower. Despite this preconception, Haskell is being used successfully in real world projects: Facebook built its spam filter with Haskell, the Commercial Haskell special interest group lists companies using Haskell, and our own success stories show some examples of successful applications of Haskell in industry.
If you want to try out Haskell right away, you can find instructions for getting started here. If you want to learn more, have a look at the FP Complete Haskell Syllabus, or look at our Haskell training.
If you’re planning to use Haskell for a project and want some help, we can provide you with Consulting services.
Subscribe to our blog via email
Email subscriptions come from our Atom feed and are handled by Blogtrottr. You will only receive notifications of blog posts, and can unsubscribe any time.