Ok

En poursuivant votre navigation sur ce site, vous acceptez l'utilisation de cookies. Ces derniers assurent le bon fonctionnement de nos services. En savoir plus.

Les Fonctions (Trois)

Une nouvelle notation

On va ici définir une nouvelle notation de la mort qui tue pour les fonctions. Un langage plus simple qu'Haskell... 

D'abord types et valeurs c'est pareil, et constructeurs de type et fonctions c'est pareil. 

Une fonction c'est d'abord un "matcheur" qui déstructure suivant ses besoins une donnée d'entrée. 

f: Int -> Int = inc = (x) (x+1) 

Mieux, x étant argument "par défaut".

f:Int->Int = x + 1 

Inutile de noter "lambda" quelquechose qui est DEJA typé ici.

 

Un constructeur de type, c'est pareil: 

Option: *->* = (T) (None, T ) // ici, "virgule" veut dire "ou"

Option: *->* = None, T

On a donc unifié valeur, fonction, type... 

Pour appliquer une fonction, on adjoint symbole fonctionnel et valeur: (inc 3) == 4

Pour  appliquer un constructeur de type, pareil.

   a: (Option Int)   fait de la valeur a une option, c'est à dire une valeur taggée par le fait d'être une option.

 

Prenons alors les 4 monades principales (la grande tétrade) et analysons les en détails, pour qu'elles forment le socle de l'évidence fonctionnelle, ce qui  manque pour VRAIMENT l'épouser et la comprendre. 

On rapellera que les 3 interdits du fonctionnel, (interdiction de la valeur nulle, interdiction de la lecture, interdiction de l'écriture) seront couverts ici par les 3 patterns fondamentaux qui les prennent en compte: comment typer la valeur nulle, la lecture et l'écriture et mieux comment typer la lecture ET l'écriture simultanée.

La monade Option

On va ici s'affranchir des valeurs nulles, explicitement typées par la valeur "None" 

Option: T->T = None, T // ici, "virgule" veut dire "ou"

A partir de là: 

Option.map : (Option T ) (  T->T')    ->    Option T' =

(x  y) (             if (x == None) None else (      x== (Option a)     (       (y then Option) a     )                       )

"x" est le premier argument, directement la valeur encapsulée par le premier argument de type Option T et y le deuxième, ce qui fait que le couple de déclaration de paramètre, "(x,y)" en début de notation, est en fait inutile...

La notation peut aussi utiliser ici un opérateur  de "parallélisme logique" autour de la virgule/"ou". 

Option.map = (None  ,   (Option a) )  (None,   ((y then Option) a)   )

"Option" sera ici, aussi, une fonction, disons ce que fait Some en Scala... (le "run", ou "point" des monades).

La notation rend la structuration/destructuration implicite, le type servant de gabarit, de traitement terminal. 

En Scala, on ferait :  

def map(x:Option[T], f: T =>T'):Option[T'] =

x match { case None => None; case Some(x) => Some(f(x))}

= Some(f(x.getOrElse (return None)))  // vla du scala hard mais qui marche.

L'expression de flatMap est exactement la même... 

Option.flatMap : Option T, T->Option[T'] =

if (x==None) None else  (x then y )

==  (None,x) (None,x then y)

Ou bien (x y) ( (None, Option(a)) y) (None, (y a) )

En Scala:

def flatMap(x:Option[T], f: T =>Option[T']):Option[T'] =

x match { case None => None; case Some(x)=> f(x)}

 

La monade Reader 

Il s'agit de modéliser la lecture pure.

Reader C : *->* = C -> T  // la valeur est une fonction de C, la configuration , vers le  type encapsulé T.

Reader.map : Reader C T, T -> T' = x then y  // comment faire plus simple ? implicitement... 

Ici, parce que typée à destination de Reader C T , y  (une fonction de T  vers T') est automatiquement composée avec une transformation vers Reader C T'... L'exécuteur de mon langage est vraiment astucieux et on pourrait être plus explicite. (Reader C T) pourrait être ainsi considéré explicitement comme un constructeur de valeur typée, avec comme argument la fonction qui le définit. 

Reader.map : (Reader C T)  ( T->T' )  =   (Reader C T') ( x then y)    

cela car (x then y ) est bien une fonction de C vers T'.

 

Reader.flatMap :  Reader C T, T -> Reader C T' =

(c) (( (x then y) c) c) 

def flatMap (

   x:Reader[C,T],

   y: T =>Reader[C,T']):Reader[C,T']=

Reader[C,T'] (c=> y(x.value(c)).value(c))

 

Reader est donc une construction utilisable pour programmer. L'idée est de retarder à l'extrême l'emploi de la configuration (de type C). Un "Reader" c'est une accumulation de calculs en fonction d'une valeur inconnue, en fait une fonction en attente de son premier argument. La composition lambda serait: 

f = for ( 

a<- (Reader Int Int) (+3)

b <- (Reader Int Int) (-2) 

) (a + b) 

f est de type (Reader Int Int) et doit être appelé (avec pour argument une "configuration" ) pour donner quelque chose. Ici "1000" est la configuration. 

f 1000 

donne (1000 + 3) + (1000 - 2) = 2001

Ainsi, la fonction d'un Reader fait office de calcul relatif par rapport à une valeur convoyé -à l'identique- par les flatMap lors d'une composition. 

On a ici la plus parfaite illustration de la fonction, du rôle et du service rendu par une monade: le transport transparent dans une composition (nécessairement faite par flatMap, y a que ça pour ça) d'une information particulière, ici la configuration. L'abstraction de cette valeur qu'on peut , dans le cas du Reader, donner après coup est le cas d'usage particulier de la monade. Ici, on a une sorte d'interprétation "retardée": les calculs de la configuration ne seront fait qu'APRES le choix et l'application de la configuration.

La monade Writer

La monade Writer est un peu l'"inverse" du Reader. Elle va être utilisée pour convoyer par flatMap le texte d'un log, modifié par ajout à chaque étape. 

Writer S : *->* = (S,T) 

Writer.map : ( Writer S T)  (T -> T') = 

x then ((z, w) ( z, y w))   // simple application of the function

Writer.flatMap : ( Writer S T) , (T -> Writer S T') =

x then ((z, w) (   (y w)  then  (p q) ( (p + z, q )  // add the logs... 

 

Et donc, 

f = for (

a<- Writer ("first line,", 33)

b<- Writer("second line", a * 2) 

)  (a + b) 

sera un Writer contenant la valeur 66 et le log "first line,second line". 

Ici, la valeur convoyée est stockée au fure et à mesure dans l'objet par le flatMap. Pas d'abstraction finale, mais l'effet est réel. Notons que ici c'est le premier Writer de la chaine de flatMap qui initie le stockage. On évite, stylistiquement, de convoyer une instance particulière dans la suite de calculs, la simple référence au "type" permettant de connecter les différents Writer entre eux  dans la chaine de flatMap exécutés et la transmission progressive de la chaine stockée, augmentée à chaque étape. 

Cela donne une capacité d'abstraction, les appels à Writer fait ici pouvant être remplacés par des appels à des fonctions quelconques retournant le Writer adapté. On évite ainsi une réification avec une instance et donc une affectation (berk berk). 

La monade State

La monade State fait tout, lecture ET écriture. 

State : *->* = S -> (S,T)

Ici on adoptera la notation "avec constructeur" du type de l'objet monadique. 

State.map : (State S T)  (T ->T' )  =

(State S T') (   x    then     ( (z w) (z, w then y) )      ) 

State.flatMap : (State S T)  ( T -> (State S T') ) =

(State S T') (  x then  (  (z w) (    (y w)(z)     )   ))

La fonction encapsulée par la monade peut changer l'état, et c'est toute l'histoire.

t = for (

a<- State (  (x + 1, x+2)  ) //  

b <- State(  (x+3, x+2)  ) // 

) (a + b) 

t est une monade Reader prête à être évaluée, comme Reader, elle est un stockeur d'expressions.

(t 1) = ( (1 + 1) + 3)  ,   2+2)

Son "état" final sera 5 et la valeur finale calculée 4, obtenue en fonction de l'état précédent (2).

 

On remarquera qu'une monade est en fait ici une fonction, disons que la valeur qu'elle encapsule est ici une fonction, c'est une monade "fonctionnelle" et que DONC, on peut et doit l'évaluer. 

Reader et State, comme monades "lisibles" ont besoin qu'on leur passe une valeur extérieure pour fonctionner. C'est ce qui les rend compliquées, alors qu'en fait on s'y fait très bien. 

Apprendre tout ça par coeur est un MUST  absolu. Merci qui? 

 

Les commentaires sont fermés.