Hello, so I will speak about two animations in Haskell using Gloss, Lens and State.
I am Julien DeHos, and I am an assistant professor in computer science, and I use Haskell mostly
for teaching functional programming.
Haskell is not the most widely used language for implementing animations, but still it
has some interesting tools, such as library bindings like SDL2.
We also have some entity component system implementations, which is a classic technique
for developing games, and we also have functional reactive programming, which is a technique
for implementing complex user interfaces, for example.
And you can find some cool projects developed in Haskell, for example the effect process,
which is a game available on Steam, that has been open sourced recently, and also the
weanimate library, which can make quite impressive animations.
In this talk, I will show how to implement several animations on concrete examples using
functional programming, and how to improve this code using some features of Haskell,
like data type, release evaluation, students library and the state and one other.
So first, let's look at a very simple example, let's say...
I want to know, can we get to a little louder?
Oh, okay.
It's a little hard to understand.
Okay.
So as a first example, let's say we want to draw a disk on the screen with a fixed radius.
To do that, we can use the Gloss library, which is a classic library in Haskell for
implementing animations and 2D graphics.
This library provides some functions for drawings, primitives, for handling user events, and the
Gloss library also provides some main loops that will run the main application.
So basically, all we have to do is to write some unlawful functions, which say how to
run the scene or how to run the user inputs, and then we pass these functions to the main
loop and that's all, we can run the program.
So let's do that.
For this first example, we don't have any particular data, we just want to draw a disk
with a fixed radius, so there is no data to remember for describing the scene.
So we can write a type, which represents the model of our application, but here we don't
need anything, so we can say it's the unit type, which means no data.
Then we have to write a function that renders the scene, so this function should take a
model and return a picture.
Here we use the solid circle function, which is provided by Gloss, to draw a disk on the
screen, and we say we want a disk with 50 pixels as the radius.
We also need a function to under user events, that function should take an event and a model
and return a new model.
This is a very classic way for modifying data in functional programming.
We can't mutate a variable because it's a side effect and pure functional programming,
we can't do that using pure functions.
So we just take the current model and return a new model, a copy of the model, which contains
the modifications.
For now, the scene is static, so we just return the same model.
And finally, to handle time, we just need a float, the elapsed time, the previous update,
and the current model, and we return the new model with the modification.
Once again, the scene is static, so for now, we return the same model.
Now we can write the main function.
We just have to set some parameters, for example, the initial value for the model, and some
parameters for the window, the background color, and the format of the animation.
Then we can call the play function, which is a main loop provided by the Gloss library,
and we just pass to this function our parameters and our under function.
This is a very classic way to do in a functional programming.
We have functions that we can pass to other functions, and we can organize the code like
this.
So we get something like this, we can run the program, it's really impressive.
Nice.
And now let's add some animations.
So let's say we want to refresh the scene every second and change the radius using a
random number.
So to do that, we can use a pseudo-random number generator.
We need to model our scene differently, so we write a type, which is model here, which
has two fields, first the current radius of the disk, and the random number generator
that we can use to update the scene.
So this is a record type in Haskell.
We have two fields, which have each of them as a name, and we can then use the function
here.
So the name of the field is also a function that can access to this field using the model.
So here we get the radius of the model, and we use that as the radius for drawing the
disk.
Of the under time function, all we have to do is to generate a new radius.
So we take the generator inside the model, and we call this function to generate a new
radius.
Since we cannot mutate the generator, we have to return a new generator for the next random
generation.
So this is why we get a new radius here and a new generator here.
And that's it, we can build and return the new model, which is the result of the function.
We need to update the main function.
We have to get a random number generator.
We can do that with this function, which gets the standard number generator from the system.
And we can also generate a first random number for the first radius of the animation.
And the model is built, is constructed here.
We get something like this, which is not so much impressive, but there is some animation.
So this is a very classic way for generating random numbers, but in Askel, we can do differently.
Since Askel has lazy evaluation, we can define an infinite list for all the radius of the
animation, and Askel will compute the numbers when it needs them.
So instead of the generator, we can use here a random list, an infinite list, and that's
all we need.
We will consume the elements in this list for having new reduces.
The unmet time function can be in fact like this.
So we have here the infinite list.
And we can just get the first element for the new radius, and the rest of the list will
be used for the next update of the scene.
In the main function, we have a function to get an infinite list.
So instead of the randomR function, we just have to call the randomRS function.
And this gives us an infinite list of random numbers, and we don't have to under a random
generator explicitly.
Let's say we want a ball that moves inside the window, and bounces against the border
of the window, and can show the result.
So we want a ball that moves inside the window, it can bounce against the border of the screen,
and if I hit on Turkey, the scene is initialized with a random velocity and a random position
for the ball.
So how can we do that?
We need more complex types, so we can first describe a ball as a position and velocity.
These fields are 2D vectors, and now the model is just the current ball and the infinite list
of the other balls we can generate randomly as we did before with the radiuses.
These types are more complex than before, because we have a model that has a ball, and
a ball has two fields which are 2D vectors.
So these vectors have x-coordinate and y-coordinate, so we have nested types which is a bit more
complex to use.
We can handle this type with a scale using standard record syntax, there is no problem
with that.
The syntax is just a little bit more complex.
So here we get the ball field of the model, and here for example we return the same model
as the argument, but we change the ball field with these balls here, which has been computed
before.
All the other fields of the model doesn't change, we still copy them, in fact.
So this function updates the scene, I have implemented it in two steps.
So first we move the ball and then we compute bounces against the border of the window.
So let's look at the update bounces function.
We have to compute the collision with the border of the windows, so we take a ball as
input and we return the ball after all the collisions have been computed.
To do that, we can use the record syntax as did before to change only the field that needs
some modifications.
But in fact, it's sometimes simpler to fully reconstruct a ball, so that's what I did here.
I have detected a collision with the left border, and I have to return this ball so
I can set explicitly what is the new position vector and the new velocity vector.
In fact, there is only two fields which are different, the x-coordinate of the position
and the x-coordinate of the velocity.
So to avoid reconstructing the ball, we can use a library in a scale which is length and
which can simplify this code.
So the length library enables us to access and modify nested types so we can go deeper
inside the type to just add a small modification.
To do that, we need to construct lenses, lenses are just functions that can access
to a data type.
And when we have these functions, we can use all the functions an operator provided
by the lens library.
So let's do that.
We can build these functions, these access functions using this function make lenses,
and that does everything for us.
So we just have to call make lenses for the ball and for the model.
And that's it.
We can use all the operators provided by the lens library.
This can look like this.
So here I return the model with two modifications, the first modification which is applying this
function to the ball field and the second modification here where I apply the update
lenses function to the ball field of the model.
And finally, the model with these two modifications is returned.
We have more than that.
For example, for the update lenses function, instead of reconstructing the ball, we can
now just getting deeply inside the type to apply some changes.
For example here, I set this value to the X field of the position field of the ball
and finally the ball is returned.
And then I can change another modification here.
I apply the negate function to the X field of the velocity field of the ball.
So I can change several modifications and go deeply inside the type to make some modification,
setting a value or applying a function.
So this is quite interesting.
We can still improve this code.
As you can see, we take a ball and we turn a new ball.
So it's just updating a ball.
And to do that, we have computed here several steps which corresponds to the collision between
all the borders of the windows.
In fact, we are modifying a ball, but we can't do that in pure functional programming.
So we have to use intermediate variables that store the modification after this collision
and this collision.
So the code is quite cumbersome and we can improve that using something in Askel, which
is called the state monad.
So the state monad is a very well-known monad in Askel, it's a very classic monad.
It's just a context where we simulate mutating a state.
So each action inside this monad is an access or a modification of the current state and
we can get the final state or another result, we can do that also.
And that works well with the lens library because the lens library provides a stateful
version of its function and operators.
So let's do that.
We can change another function like this.
Instead of applying several modifications, we can just execute the state actions defined
here.
So this is the function.
We have to, this function takes a first parameter which is the previous ball and when we have
applied all the action, the state action, we get a final state which is the final ball
that we can use to update our model.
Let's see the update monad's function.
So instead of taking a ball and returning a ball, now it's clear that we are in a state
monad and this is a state action where the current state is a ball and we can return
a value but here we don't need that so the function returns a unit.
That means that every action inside this function is now an action, a state action.
So reading the state, modifying the state and so on.
For example here we can access the postfield of the current state which is a ball.
Here we can set this value to the x field of the position field of the ball or applying
a function on the x field of the velocity field of the ball which is the current state.
Since the state monad is a monad, we can use all the features available for monads such
as the denotation so we can change several actions like this and we can also use some
functions provided for monads such as the went function.
As a result, the code is a little bit more simpler and it's clear that this is a state
action that we have a current state which is modified according to the code and then
we get the final state and this is checked by the compiler.
So to conclude, we have seen that functional programming and ASCEL using a functional programming
and ASCEL we can implement animations and this is very natural in functional programming
since we just have to pass some function to other functions like a main loop and we can
decompose and organize our code like this.
We use infinite list to generate random numbers so we don't have to use random numbers explicitly.
We just consume the elements of this list.
We also use the lens library to access or modify nested types and we can go deeply inside
these types.
Then finally, we simulate mutable state using the state monad so we can modify variable
and get the final result.
So all of this is still based on functional programming so we just manipulate pure functions
and static typing and this is quite easy to read and less work run since we have no side
effects function only depends on its argument and produce the same result if the arguments
are the same.
And all of this is checked by the compiler.
So this code, this state and the code shown here are available at this link and you can
find some information in the documentation of the libraries and see things that sit for
me.
Thank you for your attention.
Thanks to the organizer.
Thank you, Julia, and there's five minutes for questions.
If you have a question put up your hand, I'll bring the mic.
Do we know what the performance of class is like for complex applications like could
you write a complex QI in Gloss?
Do we know what the performance of Gloss is for complex display?
Gloss is based on OpenGL so it's not that slow but I don't know for very complex animations.
I believe some projects use SDR and it seems they have no problem of performance but I
have no experience more like that.
In the play function it pretty much makes the whole program pure with no I.O.
What if you do want to do any I.O. in an application?
So the Gloss library provides both two interfaces, one which is purely functional and another
where you can do I.O.
So there is a version where you can do that.
Any more questions?
Yes.
Can you explain the operators used for the lenses?
There's many, many operators.
Is the person signed?
Yes.
Okay.
So there is two versions of these operators, one which is purely functional so you just
take your data structure that it can access and return the value.
So this is such operators.
So that means we apply a modification, so the ball zero is returned after this modification.
So this is what this operator means.
Here it's for accessing another field.
So it's an X field of the position field of the ball.
And this operator says we set the value in this field and this operator says we apply
a function on the field.
And the stateful version is the same but we have an equal sign instead of the tilde.
Like a get and a set.
Yes, we can say like this.
We can say that.
Any more questions?
Okay.
Let's thank Julio.
Thank you.