Why Use Functional Programming Features?
Tags: programming.python, programming.scala, programming.exceptions
I was asked in an interview “what advantages are there to coding in a
functional style?”.
I’d use ‘functional’ a bit more loosely than “everything pure, monads, etc.”;
I’m happy with modern language idioms like pattern matching.
– I’d say the best reason is it makes explicit dangerous things which are
implicit in procedural, blub language code.
Consider a typical dictionary.get(key)
:
If the key is in the dictionary, get the value, the world is at peace and all
is well.
But writing code where things go well is easy. Here, it’s more interesting to
consider how to model “what to do when things go wrong”. – To me, returning
Optional[Value]
(as any “functional” language will do) seems the most
elegant:
Mainstream languages (like Java, Python) tend to return null
/None
for this
case.
Mainstream languages also tend to throw errors when trying to use a
null
-value as if it’s not a null
-value. So what happens is you’ll write
code assuming the value is non-null
; at some point this (implicit) assumption
will be wrong. (Let he who’s never had a NullPointerException throw the first
stone).
Optional[Value]
is kinda morally equivalent. A dictionary returns
Some[Value]
, which contains a non-null
value if the dictionary has the key,
None
if the key wasn’t present in the dictionary. (Well, technically it could
be a Some(null)
; but by the same argument, this’d be better as Some(None)
).
The syntactic difference is, every time you have an Optional[Value]
you MUST
have optional match { Some(x) => ...; None => ...; }
(or an acknowledgement
of ‘danger’, like optional.getValue()
). – With nullable variables, it’s up
to the programmer’s discipline to have if (x != null) { ... } else { ... }
;
I’d be willing to bet the else
clause is almost-always missing in code.
– Or, rather, looking at code where values are never null, you know you can’t
get NPEs. With nullable-values, you’ve to rely on (implicit) assumptions, and
maybe-probably the code is fine.
There are other ways to deal with errors.
One kinda-stupid approach would be for the dictionary to loop indefinitely
(until it has a value for the given key). In a concurrent/parallel program,
this isn’t completely stupid, & blocking-until-result may be desirable. But
still.
Throwing an exception is another approach. Python does this. It seems to me
this is kindof-sortof morally equivalent to Optional[Value]
also; the benefit
being that a value isn’t going to be null
, the drawback being it’s difficult
to tell what the program is doing:
try {
= dict.get(keyX);
x doTask1(x);
= dict.get(keyY);
y doTask2(x, y);
} catch KeyNotPresent ex {
//...
}
– Obviously if an exception is thrown above, it’s not clear whether it came
before or after doTask1(x)
; perhaps less clear is that y = dict.get
onwards
‘needs’ its own try-catch
block. It doesn’t make sense to have
KeyNotPresent
exception cover both x = dict.get
and y = dict.get
; not if
you want to be sure that you’re handling each alternative.
Error Codes, similarly, seem kinda-sorta equivalent if your program follows
discipline to handle all alternatives. Somehow Exceptions are seen as
good for allowing you
to “leave error handling code out of much of your code”. (If you don’t write
half your program logic, you don’t have to write half your program logic). –
Though, much of the advantages of Exceptions over Error Codes here also apply
to Optional
and friends.
– The advantages that Exceptions and Error Codes have over Optional[Value]
is that the latter just says “not everything went well”, the former can
describe what went wrong. (IOError, DivisionByZero, StackUnderflow,
UnsupportedOperation, etc.). This can be quickly remedied by using Either
,
i.e. keeping information about what kind of error occurred.
That’s all fine & well.
Optional
works well to model where computations may fail; Either
for when
you want to know why things went wrong (or perhaps maybe to recover depending
on what went wrong). These are more explicit than nullable values or Error
Codes.
The bigger challenge is how well functions-which-may-fail compose with other functions-which-may-fail.
importantComputation() {
fun = mayFailX()
x ;
stmt1= mayFailY()
y ;
stmt2return g(x, y);
}
With stateful code, one needs to be careful about assumptions about what
stmt1
, mayFailY
mutate. – Implicitly, you also mustn’t assume stmt2
,
g
are executed (& so must consider state when they are executed, aren’t
executed).
– While it’s true that Exceptions defer the error-handling; there’s surely
more cognitive-load required to keep the above in mind while scrolling through
code.
Functional code also isn’t necessarily so cumbersome. Optional.map
allows:
match {
optional Some(x) => doSomething(x)
None => None
}
to be written as optional.map({ x => doSomething(x) })
(or
.map(doSomething)
if you literally have a function).
Programmers tend to make mistakes when they forget to take care of cases when
things go wrong.
It’s easier to make mistakes when “something went wrong” is implicitly handled.
Functional Programming-ish code tends to be very explicit; and potentially more
concise. For writing correct, complete, safe code, this is clearly an
advantage.