class: center, middle
by Taylor Fausak
on 25 April 2016
???
Presented at the Dallas Functional Programmers meetup on 25 April 2016. http://www.meetup.com/Dallas-Functional-Programmers/events/229367392/
Abstract:
I recently spent some time building a standard library for PureScript, which is a relatively new, strongly typed, pure functional language that compiles to readable JavaScript. PureScript is heavily inspired by Haskell, so I will compare it to that, as well as other languages such as ClojureScript and its host language JavaScript.
My standard library is called Neon and it aims to address some of the problems I have seen working with Haskell’s standard library (called the Prelude) over the past couple years. I’ll talk about some of the design decisions, such as lawless type classes, as well as how it stacks up to other projects like SubHask.
I cribbed some of this from Snoyman's "Why I prefer typeclass-based libraries". http://www.yesodweb.com/blog/2016/03/why-i-prefer-typeclass-based-libraries
class: center, middle
???
Before getting into Neon, everyone should at least be familiar with the language it's written in.
PureScript is a small strongly typed programming language that compiles to JavaScript.
https://hackage.haskell.org/package/purescript:
A small strongly, statically typed programming language with expressive types, inspired by Haskell and compiling to Javascript.
???
These are the official definitions.
Colloquially, you can think of PureScript as Haskell for JavaScript. There are many other similar projects, like GHCJS and Elm. PureScript is different because it has no runtime and adds extensible records to Haskell.
If you want to know more, watch my "Better know a language: PureScript" talk: http://taylor.fausak.me/2015/10/22/better-know-a-language-purescript/
???
This is how I think of PureScript in my head.
class: center, middle
https://projecteuler.net/problem=1:
If we list all the natural numbers below 10 that are multiples of 3 or 5, we get 3, 5, 6 and 9. The sum of these multiples is 23.
Find the sum of all the multiples of 3 or 5 below 1000.
???
I like this example because it includes many of the core elements in programming: looping (or filtering), conditionals, and output.
That being said, it's obviously simplistic.
We will explore how to solve this problem in a variety of languages and paradigms.
The goal here is to give you an idea of what Neon looks like before jumping into the deep end.
sum = 0
for (x = 1; x < 1000; ++x) {
if (x % 3 == 0 || x % 5 == 0) {
sum += x
}
}
console.log(sum)
???
This is the traditional imperative approach. With libraries like Lodash, this could be made more functional. But with vanilla JavaScript, this is almost the best we can do.
(->> 1000
range
(filter (fn [x]
(or (zero? (mod x 3))
(zero? (mod x 5)))))
(reduce +)
println)
???
Clojure is functional, so we can solve this problem with the functional
approach using filter
and reduce
.
I won't get into the pros and cons of Clojure's syntax, but I think we can all
agree ->>
isn't immediately obvious. Plus it can be annoying to choose
between that and ->
and as->
and some->
.
main
= print
. sum
. filter (\ x -> x `mod` 3 == 0 || x `mod` 5 == 0)
$ [1 .. 999]
???
Another functional approach. There is a lot of weird syntax here with the backslash, arrow, backticks, and double dot. Plus there are some weird operators with dot and dollar. Not to mention the whole thing reads backwards.
main
= print
<<< sum
<<< filter (\ x -> x `mod` 3 == 0 || x `mod` 5 == 0)
$ 1 .. 999
main
= 1 .. 999
# filter (\ x -> x `mod` 3 == 0 || x `mod` 5 == 0)
>>> sum
>>> print
???
Two options here: bottom up or top down. I personally prefer the second one because it's easier to follow.
This has the same problems as the Haskell example. Weird syntax and weird operators. The triple arrows are better than the dot because they suggest which direction things are moving in. But the dollar and hash are useless.
main = 1
:upTo 999
:filter (divisibleBy 3 || divisibleBy 5)
:sum
:print
???
Obviously my favorite choice. No weird syntax and only two operators.
You may think this is unfair because Neon provides the divisibleBy
helper.
You could also do \ x -> x % 3 == 0 || x % 5 == 0
if you wanted to.
class: center, middle
???
Now that we know what Neon looks like, let's dive into why it looks like that.
Type classes are the primary means of abstraction in PureScript. If you want one function to work on two data types, you need a type class.
addInt :: Int -> Int -> Int
addInt x y = {- ... -}
addInt 2 3 -- 5
addNumber :: Number -> Number -> Number
addNumber x y = {- ... -}
addNumber 2.0 3.0 -- 5.0
addInt 2.0 3.0 -- ERROR
addNumber 2 3 -- ERROR
???
It's easy to define functions that work on one data type. (I've left out the definitions here because they would use the FFI.) But how can you make one function that works with both integers and numbers?
This isn't Go, we should be able to do this.
A note on syntax: In PureScript, a literal without a decimal point is an Int
.
If it has a decimal point, it is a Number
.
module Int where
add :: Int -> Int -> Int
add x y = {- ... -}
module Number where
add :: Number -> Number -> Number
add x y = {- ... -}
import qualified Int
import qualified Number
Int.add 2 3 -- 5
Number.add 2.0 3.0 -- 5.0
Int.add 2.0 3.0 -- ERROR
Number.add 2 3 -- ERROR
???
One way around this problem is to define each function in its own module and import them qualified. This technically works, but using modules like this is a huge pain.
class HasAdd a where
add :: a -> a -> a
instance intHasAdd :: HasAdd Int where
add = addInt
instance numberHasAdd :: HasAdd Number where
add = addNumber
add 2 3 -- 5
add 2.0 3.0 -- 5.0
???
With a type class we can use the same function on both integers and numbers.
infixl 5 add as +
2 + 3 -- 5
2.0 + 3.0 -- 5.0
???
By defining an operator that uses the type class function, we can get operator
overloading. Now if you want to define +
for your data type you just need to
add an instance for the HasAdd
type class.
class: center, middle
???
Now for one of the contentious parts of type classes: Laws. Laws are suggestions about how to implement a type class. Every other language solves this problem with documentation.
The word "law" implies more gravitas than they actually have. Even in the standard library for Haskell there are instances that don't follow the law!
class Eq a where
eq :: a -> a -> Bool
infix 4 eq as ==
Laws:
- Reflexive:
x == x
is true - Symmetric: if
x == y
theny == x
- Transitive: if
x == y
andy == z
thenx == z
???
Laws tell you how to implement your instances. They also tell you how to use the type class. But I'll bet you could've guessed these laws based on your experience in other languages.
class Semigroup a where
append :: a -> a -> a
infixr 5 append as <>
Laws:
- Associative:
(x <> y) <> z
is the same asx <> (y <> z)
???
This is one of the simplest type classes. It has one law that says you can group terms up however you like. When you write an instance, you should make sure that you follow the laws. That way other people can use your data type just like any other, with respect to semigroup-ness.
A good way to make sure you actually follow the laws is to use Quick Check. It will turn the law definition into a test case that randomly generates values and sees if they follow the laws.
class: center, middle
???
I could give an entire talk about laws. Suffice to say, Neon doesn't have laws per se. It does suggest how to implement and use type classes. And it uses Quick Check to test its own instances. But it doesn't (and won't!) mention laws in the documentation.
class: center, middle
???
In Neon, none of the type classes require any of the others. If you want to implement subtraction but not addition, you're free to do that. If you're writing a function and you want both, ask for both.
absoluteValue :: (HasLess a, HasSubtract a, HasZero a) => a -> a
absoluteValue x =
if x < zero
then zero - x
else x
???
This is like duck-typing applied to a static language. As long as your data type implements these type classes, it can use this function.
Also, you should be able to guess how this function was written without actually seeing the definition.
class (HasMap f) <= Functor f
class (Functor f, HasApply f) <= Apply f
class (Apply f, Functor f, HasPure f) <= Applicative f
class (Applicative m, HasChain m) <= Monad m
???
This doesn't prevent you from defining type classes that compose others
together. This way you can get back your Monad
hierarchy without needing any
support from me.
class: center, middle
???
I think the traditional type class names (given in the last slide, for example) are confusing. They are named for mathematicians, not programmers.
import Prelude
newtype MyList a = MyList (Array a)
map (_ + 1) (MyList [1, 2, 3])
Error found:
No type class instance was found for
Prelude.Functor MyList
???
What the heck is a functor? I was trying to map
over my list.
import Neon
newtype MyList a = MyList (Array a)
map (_ + 1) (MyList [1, 2, 3])
Error found:
No type class instance was found for
Neon.Class.HasMap.HasMap MyList
???
Ah, that's better. I didn't implement map
for MyList
.
(The fully-qualified class name is a bit unfortunate.)
Edward Kmett
[Functor] gives me access to 70 years worth of documentation
???
I think that's great, but why does it have to be the actual class name? I think
it's sufficient to mention in the docs for HasMap
that is is a functor. Then
if you know what that is you'll be enlightened. If you don't and you're
curious, you can go look it up.
https://en.wikipedia.org/wiki/Functor:
In mathematics, a functor is a type of mapping between categories which is applied in category theory. Functors can be thought of as homomorphisms between categories. In the category of small categories, functors can be thought of more generally as morphisms.
???
Oh... ok.
class: center, middle
add "ab" "cd"
-- "cdab"
-- ... what?
???
The order of arguments in Neon is "backwards". This is to make function
application with the :
operator more useful.
"cd" :add "ab"
-- "cdab"
???
By putting the "subject" last and creating an operator that applies values to functions, we can write code that looks object oriented. I think this is a huge win because it allows you to work left-to-right. You can start with a simple value and keep working with it until you have what you want.
This also supports easy REPL-driven development. You never have to go back to the front of the line. You can keep adding to the end.
Also this could allow for better IDE integration. Once you type :
, the IDE
could look at the type of the previous thing and suggest functions that take
that type.
This works even without an IDE by doing x :_
to get a typed hole for that
function. Then you can take that type and plug it into Pursuit to find a
function that you need. (Note that this requires you to have a type signature.)
class: center, middle
???
Operators are contentious in Haskell and PureScript. Anyone can freely define them. Neon makes a point to not have many.
infixl 8 call as :
infixr 7 power as ^
infixl 6 multiply as *
infixl 6 divide as /
infixl 6 remainder as %
infixl 5 add as +
infixl 5 subtract as -
infix 4 equal as ==
infix 4 notEqual as !=
infix 4 greater as >
infix 4 greaterOrEqual as >=
infix 4 less as <
infix 4 lessOrEqual as <=
infixr 3 and as &&
infixr 2 or as ||
???
This is every operator that Neon provides. You'll find that there's a lot of
overlap with JavaScript. That's intentional. The only addition is :
. The only
obvious things missing are bitwise operators like |
. Unfortunately those are
part of PureScript's syntax.
class: center, middle