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.
Concise syntax
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.
More powerful type system
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.
Zero-cost newtypes
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.
Track side effects in the type system
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.
No conflicting implicit conversions
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.
Superior type inference
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.
More advanced features
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.
Proper tail call optimization
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.
Language interoperability
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:
- The Foreign Function Interface (FFI) allows calling C functions from Haskell code. It is very easy to use, and allows interoperation with all languages that also have C interoperability (such as C++, Python, and Ruby). A lot of popular C libraries have Haskell bindings using the FFI.
- Furthermore, it is possible to embed snippets of C code directly into your Haskell code, via inline-c.
- For data analysis and visualization, you can embed R code with inline-r.
- With the recent addition of inline-java, it is straightforward to call Java functions from Haskell (although Haskell will not share a heap with Java, as Scala does).
Haskell is ready for production
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.
Get Started
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.