It's been a year since I've written any Mercury (the purely logic/functional programming language), but there's one feature I miss. It's nothing fancy, and certainly one of the less interesting features Mercury has to offer.
The Problem
To understand this particular feature let's look at where other languages fail. Here's an example in Swift. Suppose we want to define the speed of every vehicle in existence. Lucky for us, there are only three. Here's the code:
First we have a type:
enum Vehicle {
case car(String, String, Int)
case jet(String)
case segway(String)
}
And the implementation:
func speed(vehicle: Vehicle) -> Int {
switch vehicle {
case .car(let make, let model, let year):
// ... Some Calc ...
return res
case .jet:
// ... Some Calc ...
return res
case .segway:
// ... Some Calc ...
return res
}
}
Ok, that's fine. But as the number of vehicles grows you'll find your function grows too. Here with only seven cases it's already getting a little long.
func speed(vehicle: Vehicle) -> Int {
switch vehicle {
case .car(let make, let model, let year):
// ... Some Calc ...
return res
case .jet:
// ... Some Calc ...
return res
case .segway:
// ... Some Calc ...
return res
case .boat
// ... Some Calc ...
return res
case .airship
// ... Some Calc ...
return res
case .airboat
// ... Some Calc ...
return res
case .hyperloop
throw NotImplementedError()
}
}
So maybe we chop it up.
func speed(vehicle: Vehicle) -> Int {
switch vehicle {
case .car(let make, let model, let year):
carSpeed(make, model year)
case .jet:
jetSpeed()
case .segway:
segwaySpeed()
...
}
}
func carSpeed(make: String, model: String, year: Int) -> Int {...}
func jetSpeed() -> Int {...}
func segwaySpeed() -> Int {...}
...
Kind of ugly!
Either you suffer the longer function body or you grimace at repeatedly calling similar functions over and over again.
If you opt for helper functions you'll repeat the associated values for each type (as in the case of func carSpeed
).
If only there were a cleaner way...
Mercury
There is, and Mercury has it. Mercury allows you to separate implementations by enum value. I'll show you what I mean.
First, here's our enum type.
:- type vehicle
---> car(str, str, int)
; jet(str)
; segway(str);
And our predicate definition:
:- pred speed(vehicle::in, int::out) is det.
Thanks to pattern matching we can write a block for each vehicle at the top level:
speed(car(Make, Model, Year), Speed) :-
% ... Some Calc ...
Speed = Res.
speed(jet(_), Speed) :-
% ... Some Calc ...
Speed = Res.
speed(segway(_), Speed) :-
% ... Some Calc ...
Speed = Res.
Ok, what's so great about this? Well, three things:
- I don't have to write any
switch
code – the switch is implied by the function definition. - The implementations are separated at the top level, so the blocks are short and easy to read.
- The compiler still checks that I've implemented all cases (This is
what the
det
mode is doing.)
The first two points come for free thanks to unification. Prolog can do as much, but Mercury adds the compile-time type-checking that Prolog lacks.
Other Solutions
Of course this isn't the only solution, some languages solve this by turning enums into classes. Here's Kotlin:
sealed class Vehicle {
abstract fun speed(): Int
}
data class Car(val make: String, val model: String, val year: Int) : Vehicle() {
override fun speed(): Int {
// ... Some Calc ...
return res
}
}
data class Jet(val name: String) : Vehicle() {
override fun speed(): Int {
// ... Some Calc
return res
}
}
data class Segway(val name: String) : Vehicle() {
override fun speed(): Int {
// ... Some Calc
return res
}
}
This works well enough, but what if you don't own the type you're switching on? Maybe you've imported it from another library. Well now you need to add an extension of sorts. And good luck implementing that without running into the initial problem.