PureScript & Pux
bit.ly/purescript-pux-talk
bit.ly/purescript-pux-repo
Typed
Pure
Functional
PureScript is a typed, pure, functional programming language that
compiles to JavaScript. It's inspired by Haskell, so if you're
familiar with Haskell, or Elm it will look familiar.
Typed
Types are known at compile time; they're static
Types can be inferred by the compiler
In PureScript, types are known at compile time. The developer can
include type signatures, but isn't required to because the
compiler can infer them. However, in practice they are almost never omitted.
Pure
inputs -> outputs
No side-effects
All functions in PureScript are pure, which means they just map
inputs to outputs and no matter how many times you call a function
with the same input you'll get the same output.
Pure functions are referentially transparent, which means you can
replace the functional call with it's return value and there is
no change to your program.
Pure functions can't have side effects. No logging to the console,
no Date/Time, no HTTP requests, and no DOM. These functions are
not referentially transparent.
Effects As Data
Eff (Effect) Result
f :: Eff (dom :: DOM) Location
g :: Eff (now :: NOW) Instant
h :: UserId -> Aff (ajax :: AJAX) User
So what good is a language that is made up of functions that can't
do anything? I mean, eventually you're going to want to render
something to the DOM, or get some data from a server, right?
In pure languages, side-effects are encoded into types. PureScript
uses the Eff type to wrap up all of the effects our program has.
The type tells you the type of effect and the type of result
Here are a few examples.
Don't worry if this code makes no sense right now, what's
important is that in PureScript we can look at the type and know
whether or not it creates a side effect. What's more, we can know,
just from the type, exactly what type of effect it is.
Functional
Literally just programming with functions.
A lot of time and effort has gone in to giving functional programming
very involved definitions, but that's really all it is.
You can program with functions in a lot of languages; JavaScript,
Ruby, Scala, but they also allow you to program in other styles.
PureScript, on the other hand, does not.
Functions
add :: Number -> Number -> Number
add x y = x + y
add 10 11
arrayMap :: (a -> b) -> [a] -> [b]
arrayMap = map
This is how a function is defined in PureScript. The first line
is the type. It says "the function add takes two numbers and returns
another number". The "::" separates the name from the type. The parameters
are separated by "->" and the last item in the type is the return type.
And this is how you call a function: the name of the function
followed by the arguments, separated by spaces.
Functions can also be polymorphic. The type of "arrayMap" says it takes
a function from some type "a" to some type "b" and a list of "a's",
and it will return a list of "b's". "a" and "b" are type variables.
So this function works with any "a" and any "b", and it's important to
note that "a" and "b" don't have to be different, but they can be.
Data Types
data RGB = Red | Green | Blue
data Action = AddUser User | UpdateName String
data User = User String Number
There are a couple of different ways to define data types in
PureScript.
Here we defined a type called RGB, with three possible values:
"Red", "Green", or "Blue". The | in between each value means "or".
This is called a sum, or union type. It's similar to an enum, but
more powerful.
This next example creates an "Action" type, with two possible
values: "AddUser" or "UpdateName". This is another example of a
sum type, but each of these comes with some extra information.
"AddUser" comes with a "User" and "UpdateName" inlcudes a "String".
In this final example, we have a data type for a "User" and in order
to have a "User" it must have a "String" and a "Number". Unfortunately,
we have no keys, so it's not clear what the String & Number are
used for.
Records
type User =
{ name :: String
, age :: Number
}
Which brings us to records, which are like object literals
in JavaScript; just simple key/value pairs. This version of User
is essentially the same as the previous one, except we now have
some context for those values.
Here we are creating a type alias for a record. We're not
introducing a new data type, instead what this says is, "We're
going to call records that have a 'name' that is a String and an
'age' that is a Number, a User".
An important note with aliases is that they only provide a new way
to talk about existing types, but no additional compile time
benefits. If I create an alias for String and call it Name, it's
still just a string. Functions that take a Name as an argument can
still be called with any String.
Newtype
newtype Name = Name String
newtype Age = Age Number
newtype User = User { name :: Name
, age :: Age
}
A newtype is a way to wrap up an existing type. Like aliases, they
give us a different way to talk about existing types, newtypes give
us compile time benefits that aliases don't.
One benefit of newtypes is that they come with no overheard. What
that means is that the runtime representation is exactly the same
as the existing type, but they are distinct from the point of view
of the type system.
newtypes allow us to write new typeclass instances for existing
types. We'll touch on typeclasses in a moment, but this means that
the newtype "Name" will be a string at runtime, but can have
different behavior than a normal string; for example,
in how we compare for equality.
Pattern Matching
Pattern matching allows us to write function definitions based
on the value of the input. It's very similar to a "switch" block.
royGeeBiv :: RGB -> String
royGeeBiv R = "Roy"
royGeeBiv G = "Gee"
royGeeBiv B = "Biv"
In this example, we provide a definition for each value of the "RGB"
data type we defined earlier, and return a string.
data UserAccount =
Guest
| Authenticated { name :: String }
greetUser :: UserAccount -> String
greetUser Guest = "Wanna create an account"
greetUser (Authenticated { name }) = "Welcome back " <> name
Here we create a UserAccount type that is either a Guest,
which has no additional data, or an Autheticated user with a name.
We provide a definition for each and in the the Authenticated
case we pull the name out and append it to the "Welcome back "
message.
isItOne :: Int -> Boolean
isItOne 1 = true
isItOne _ = false
Pattern matches must be exhaustive, or your function must be
explicitly marked as Partial, meaning it doesn't handle all inputs.
You can exhaust all patterns either by handling each possible
input or by using a catchall.
Here we match the literal value 1, but it wouldn't be
possible to match every possible Int value. Instead we use an
underscore which matches any value. Patterns are checked
in order, so they should be defined from most specific to least.
Fancy Types
Monoid
Functor
Monad
class Eq a where
eq :: a -> a -> Boolean
Fancy types. If you're familiar with Elm, you might have heard this
term used to describe some of the types in languages like Haskell
or PureScript. Things like Monoid, Functor and Monad. Most of the
time what people are talking about are typeclasses; which Elm
doesn't have. Monoid, Functor & Monad are all typeclasses.
A typeclass is just a way to abstract out common functionality. If
you're familiar with interfaces in C# or Java, or templates in C++,
it's the same idea.
The reason they have a reputation for being "fancy" or "scary", in
my opinion, is because things like Functor or Monad are more abstract
that what most of use are used to.
The "Eq" typeclass, for example, is much more concrete. It lets us
define how equality works for our types. To make a data type an instance
of this type class, or in C#/Java, to implement this interface, we
need to write an eq function that can look at two values of our type
and check for equality.
Pux
Pux is a PureScript interface to React, similar to the Elm app
It is a simple pattern for modular, nested components
that are easy to test, refactor, and debug...
http://www.alexmingoia.com/purescript-pux/
Alright. On to Pux.
Pux is a PureScript interface to React, similar to the Elm app
architecture. It is a simple pattern for modular, nested components
that are easy to test, refactor, and debug - making it simple and
straightforward to build complex web applications.
So what is the Elm Architecture?
The Elm Architecture?
The Elm Architecture is a simple pattern for architecting webapps.
It is great for modularity, code reuse, and testing.
https://guide.elm-lang.org/architecture/
The Elm Architecture is a simple pattern for architecting webapps.
It is great for modularity, code reuse, and testing.
The pattern breaks modules into three pieces. The "State", which
represents the state of your application or component. An "Update"
function which transforms your state based on "Action"s. And a "View"
function that renders the "State".
Users of Redux may recognize this pattern as it was the inspiration
for the library.
An Example
We're going to take a look at a counter app. Press a
plus button and the count goes up. Press and minus button and it
goes down.
data Action = Increment | Decrement
type State = Int
init :: State
init = 0
Next comes our State, and in this case we just going to use an Int,
but we've given it an alias to make things easier to follow. We also
give an initial state for our counter.
update :: Action -> State -> State
update Increment state = state + 1
update Decrement state = state - 1
Next is the Update function which takes an Action, the current state,
and returns the new state. We pattern match on each type of Action
and either increment or decrement the counter.
view :: State -> Html Action
view state =
div
[]
[ h1 [ className "page-title" ] [ text "Counter" ]
, button [ onClick (const Increment) ] [ text "Increment" ]
, span [] [ text (show state) ]
, button [ onClick (const Decrement) ] [ text "Decrement" ]
]
Finally we have the View function which takes the current State
and returns some Html that can perform the types of Actions we
described above.
Pux provides a DSL for constructing HTML. Each element type has a
a corresponding function. Each element function takes two arguments,
a list of attributes, and a list of children.
So we have an H1, an increment button, a span that shows the count,
and a decrement button. When the user clicks a button our Update
function is called with the correct action, and state. Then the View
function is called to render the new state.
Why
So why would you want to program like this? Programming with pure
functions, immutable data, and types puts a lot of restriction on
the developer, what's the benefit? For me the benefit is in
testing, reliability, and reasoning.
Testing
When a function just maps inputs to outputs and can have no side
effects, the testing is far simpler. There is no orchestration,
mocking, etc. Adding types can completely eliminate entire
classes of test.There's no need to test what some function, that
is supposed to accept number, does when you give it a string.
Reliability
A common saying in Haskell/PureScript/Elm languages is, "If it
compiles, it works." This is mostly a joke, but there putting
these constraints on your code greatly improves correctness leading
to fewer Bugs. Richard Feldman of No Red Ink, the largest
production user of Elm, is fond of saying that their Elm code has
had zero runtime errors in over a year.
Reasoning
update :: Action -> State -> State
The case of reasoning is best described with an example.
What can you tell me about what this function does? It could do a
lot things really. We don't really know what it does, but we
absolutely know what it doesn't do: It doesn't use anything other
than some "Action" & "State", and it cannot have any effects; No
logging, no HTTP requests, no DOM.
But types can tell us a fair bit about what a function actually
does. The types we use in our function can so limit what a function
is allowed to do with it's arguments, that we can, in some cases
be left with only one possible implementation. How that is possible
is beyond the scope of this talk, unfortunately.
By contraining ourselves to pure functions, static types, with
effects built into the type system, we are able to understand much
more about our code. Without them, we're left with names, and sifting
through numerous lines of source code.