Bonjour,
Ce message est le premier d' une série que j' espère longue et enrichissante pour le plus grand nombre d' entre-vous.
Suite à la lecture de l' ouvrage de Paul Hudak: "The Haskell School of Expression", j' ai découvert le monde un peu particulier de la Programmation Fonctionnelle Réactive.
Après avoir lu une bonne partie des articles publiés sur le sujet - et disponibles assez facilement, càd sur internet, j' ai eu l' idée de mettre en pratique ces recherches dans le cadre d' une implémentation en F#.
Et ce avec les objectifs suivants:
- Mieux comprendre ce domaine très particulier.
- Me permettre d' évaluer F# dans un cadre vraiment fonctionnel.
Ceci étant dit, je ne vais pas ici faire une présentation exhaustive de ce qu' est la Programmation Fonctionnelle Réactive. D' autres l' ont fait - et certainement mieux que moi - Cf les livres et articles ci-dessus pour une bonne introduction.
Néanmoins, rien n' empêche un petit rafraîchissement de mémoire.
Un programme fonctionnel réactif est un programme qui est écrit dans un langage fonctionnel et qui produit des résultats qui dépendent de façon continue du temps d' une part et d' événements discrets d' autre part.
Je commencerais d' abord par examiner comment il est possible de représenter en F# une quantité variant de façon continue dans le temps. Dans le monde de la FRP, une telle quantité est souvent appelée un Behavior.
La solution le plus évidente est de représenter cette quantité par une fonction qui prend un temps t en argument et retourne la valeur de cette quantité au temps t. Donc si on considère que le temps est représenté par un float, on obtient le type générique Behavior.
#light
type Time = float
type 'a Behavior = Time -> 'a
En fait, on utilise de préférence la formulation suivante. La raison de cela apparaîtra un peu plus tard dans ce blog.
type 'a Behavior = Beh of (Time -> 'a)
A l' aide de cette déclaration, on peut déjà définir un certain nombre de Behavior simples
Type de Behavior | Implémentation |
Une quantité constante égale à 1 | let oneB = Beh (fun _ -> 1.0) |
Une quantité constante égale à "hello world!" | let helloB = Beh (fun _ -> "Hello World!") |
Un Behavior qui représente le temps | let timeB = Beh (fun t -> t) |
Un Behavior qui représente le temps multiplié par 3 | let time3B = Beh (fun t -> t * 3.0) |
Une fonction sinus du temps | let sinB = Beh (fun t -> System.Math.Sin t) |
Une fonction sinus du triple du temps | let sin3B = Beh (fun t -> System.Math.Sin (t * 3.0)) |
A stade il est déjà possible de se simplifier la vie en définissant certains combinateurs utiles.
constB: permet de transformer une constante en un Behavior
//val constB : 'a -> 'a Behavior
let constB v = let bf t = v
in
Beh bf
let oneB = constB 1
let helloB = constB "Hello World!"
En ce qui concerne les 3 derniers Behaviors la situation est un petit peu plus complexe. Par exemple, time3B ressemble fortement à timeB et il est possible d' exprimer time3B en fonction de timeB:
let time3B = let (Beh time) = timeB
let bf = fun t -> 3.0 * (time t)
in Beh bf
time3B peut encore être transformé en déplaçant timeB et la fonction ((*) 3.0) en dehors du corps de l' expression de time3B:
let time3B = let liftB f b = let (Beh bf) = b
let lbf = fun t -> f (bft)
in Beh lbf
in liftB ((*) 3.0) timeB
liftB étant indépendant de tout le reste, on peut le définir comme un combinateur à part entière:
// val liftB : ('a -> 'b) -> 'a Behavior -> 'b Behavior
let liftB f b = let (Beh bf) = b
let lbf = fun t -> f (bf t)
in Beh lbf
Dés lors, il est maintenant très simple de définir time3B mais également sinB et sin3B à l'aide de liftB:
let time3B = liftB ((*) 3.0) timeB
let sinB = liftB System.Math.Sin timeB
let sin3B = liftB System.Math.Sin (liftB ((*) 3.0) timeB)
On voit que l' utilisation de liftB permet de combiner fonctions et Behaviors afin de produire de nouveaux Behaviors. Par contre l' écriture est encore assez lourde.
Il est possible de la simplifier en regardant de plus près la signature de liftB. Il s' agit d' une fonction qui prend comme argument une fonction transformant une valeur de type 'a en une valeur de type 'b et retourne une fonction qui transforme un 'a Behavior en un 'b Behavior. liftB est donc une fonction qui monte ou transforme (lift en anglais - d' où le nom) une fonction définie sur des types quelconques ('a, 'b) vers une fonction définie sur des Behaviors.
Dés lors, on peut définir:
let sinF = liftB System.Math.Sin
let sinB = sinF timeB
let tripleF = liftB ((*) 3.0)
let sin3B = sinF (tripleF timeB)
De la même façon, des combinateurs permettant de lifter des fonctions a plusieurs arguments peuvent être définis:
//val lift2B : ('a -> 'b -> 'c) -> 'a Behavior -> 'b Behavior -> 'c Behavior
let lift2B f b1 b2 = let (Beh bf1) = b1
let (Beh bf2) = b2
let nbf t = f (bf1 t) (bf2 t)
in Beh nbf
//val lift3B : ('a -> 'b -> 'c -> 'd) -> 'a Behavior -> 'b Behavior -> 'c Behavior -> 'd Behavior
let lift3B f b1 b2 b3 = let (Beh bf1) = b1
let (Beh bf2) = b2
let (Beh bf3) = b3
let nbf t = f (bf1 t) (bf2 t) (bf3 t)
in Beh nbf
Quelques exemples de fonctions liftées.
// val ( .* ) : (int Behavior -> int Behavior -> int Behavior)
let (.*) = lift2B (*)
// val ( ./ ) : (int Behavior -> int Behavior -> int Behavior)
let (./) = lift2B (/)
// val mapB : ('a -> 'b) Behavior -> 'a list Behavior -> 'b list Behavior
let mapB f b = (lift2B List.map) f b
Remarque: mapB est définie comme une fonction à deux arguments car si tel n' était pas le cas, le compilateur F# reporterait la presque omniprésente erreur de "Value restriction".
error FS0030: Value restriction. Type inference has inferred the signature
val mapB : (('_a -> '_b) Behavior -> '_a list Behavior -> '_b list Behavior)
Either define 'mapB' as a syntactic function, or add a type constraint to instantiate the type parameters.
Et pour terminer ce premier message, voici quelques fonctions permettant d' exécuter un Behavior.
// val runOne : 'a Behavior -> Time -> 'a
let runOne b t = let (Beh bf) = b
in
bf t
// val runList : 'a Behavior -> Time list -> 'a list
let runList b t = let (Beh bf) = b
in
List.map bf t
// val runSeq : 'a Behavior -> seq<Time> -> seq<'a>
let runSeq b t = let (Beh bf) = b
in
Seq.map bf t