La réduction et la métaphore de la grande roue


Voici une petite anecdote sur la manière dont j’aborde l’enseignement de certains concepts à autrui.

Récemment, j’ai dû introduire quelques concepts d’Elm à un collègue qui avait une certaine expérience avec React et Redux. L’un de ces concepts était List.foldl, une fonction de réduction qui existe dans de nombreux langages, et qui correspond à Array#reduce en JavaScript.

Le collègue avait du mal à comprendre le concept dans son ensemble, alors j’ai tenté d’utiliser une métaphore ; j’ai pensé à une grande roue à côté d’un lac, avec quelqu’un dans l’un de ses paniers tenant un seau, remplissant le panier d’eau du lac chaque fois que le panier revient au sol.

Ouais, je sais.

Alors qu’il me regardait comme si j’étais un fou, et sachant qu’il avait déjà utilisé React et Redux, je lui ai dit que c’était comme les fonctions réductrices qu’il avait probablement déjà utilisées.

Nous avons commencé à écrire un réducteur Redux standard en JavaScript pur :

function reducer(state, action) {
    switch(action.type) {
        case "EMPTY": {
            return init
        }
        case "ADD_WATER": {
            return {...state, water: state.water + 1}
        }
    }
}

Il a fait « ah oui, je connais ça ». Bien ! Nous pourrions utiliser cette fonction de manière itérative :

// Construction de l'état étape par étape
const init = {water: 0}
let state = init
state = reducer(state, {type: "ADD_WATER"})
state = reducer(state, {type: "EMPTY"})
state = reducer(state, {type: "ADD_WATER"})
state = reducer(state, {type: "ADD_WATER"})

console.log(state) // {water: 2}

Ou en utilisant Array#reduce :

// Utilisation d'Array#reduce et d'un tableau d'actions
const actions = [
    {type: "ADD_WATER"},
    {type: "EMPTY"},
    {type: "ADD_WATER"},
    {type: "ADD_WATER"},
]

const init = {water: 0}
const state = actions.reduce(reducer, init)
console.log(state) // {water: 2}

Ainsi, je pourrais réutiliser la métaphore de la grande roue :

  • state représente l’état du panier de la roue (et la quantité d’eau qu’il contient)
  • init est l’état initial du panier de la roue (il ne contient pas encore d’eau)
  • actions sont la liste des opérations à effectuer chaque fois que le panier atteint le sol à nouveau (ici, remplir le panier avec de l’eau du lac, parfois vider le panier)

Pour information, oui, mon collègue me regardait toujours bizarrement.

Nous avons continué et décidé de réimplémenter la même chose en Elm, en utilisant foldl. Sa signature de type est :

foldl : (a -> b -> b) -> b -> List a -> b

Wow, cela semble compliqué, surtout quand on débute en Elm.

En Elm, les signatures de type séparent chaque argument de fonction et la valeur de retour par une flèche (->) ; donc, décomposons celle de foldl :

  • (a -> b -> b), le premier argument, signifie que nous voulons une fonction, prenant deux arguments typés a et b et retournant un b. Cela ressemble beaucoup à notre fonction reducer en JavaScript ! Si c’est le cas, a est une action et b un état.
  • le prochain argument, typé b, est l’état initial à partir duquel nous commençons à réduire notre liste d’actions.
  • le prochain argument, List a, est notre liste d’actions.
  • Et tout cela doit retourner un b, donc un nouvel état. Nous avons la définition exacte de ce que nous recherchons.

En fait, notre propre utilisation de foldl aurait été beaucoup plus évidente si nous avions vu cela initialement, en remplaçant a par Action et b par State :

foldl : (Action -> State -> State) -> State -> List Action -> State

Note : si tu as encore du mal avec ces a

et b, tu devrais probablement lire un peu sur les Types Génériques.

Notre implémentation minimaliste résultante était :

type Action
    = AddWater
    | Empty

type alias State =
    { water : Int }

init : State
init =
    { water = 0 }

actions : List Action
actions =
    [ AddWater
    , Empty
    , AddWater
    , AddWater
    ]

reducer : Action -> State -> State
reducer action state =
    case action of
        Empty ->
            init

        AddWater ->
            { state | water = state.water + 1 }

main =
    div []
        [ -- Construction de l'état étape par étape, affiche { water = 2 }
          init
            |> reducer AddWater
            |> reducer Empty
            |> reducer AddWater
            |> reducer AddWater
            |> toString >> text

        -- Utilisation de List.foldl, affiche { water = 2 }
        , List.foldl reducer init actions
            |> toString >> text
        ]

Nous avons rapidement ébauché cela sur Ellie. Ce n’est pas graphiquement impressionnant, mais ça fonctionne.

C’était ça, c’était plus évident de cartographier les choses que mon collègue connaissait déjà sur quelque chose de nouveau pour lui, alors qu’en fait, c’était exactement la même chose, exprimée légèrement différemment d’un point de vue syntaxique.

Nous avons également expliqué que l’Architecture Elm et la fonction traditionnelle update étaient essentiellement une projection de foldl, Action étant habituellement nommé Msg et State Model.

Le truc amusant, c’est que le design de Redux a lui-même été initialement inspiré par l’Architecture Elm !

En conclusion, voici quelques points à retenir lorsque tu fais face à quelque chose de difficile à comprendre :

  • commence par trouver une métaphore, même ridicule ; cela aide à résumer le problème, à exprimer ton objectif et à t’assurer que tu as une vue d’ensemble de celui-ci ;
  • découpe le problème en les plus petits morceaux compréhensibles que tu peux, puis passe au suivant plus grand lorsque tu as terminé ;
  • essaie toujours de cartographier ce que tu essaies d’apprendre sur des choses que tu as déjà apprises ; les expériences passées sont de bons outils pour cela.

Partagez c'est sympa !

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *