Pourquoi y a-t-il tant de fonctions `map` pour différents types en F #

9

J'apprends le F #. J'ai commencé FP avec Haskell, et je suis curieux de savoir cela.

Étant donné que F # est un langage .NET, il semble plus raisonnable pour moi de déclarer l'interface comme Mappable, tout comme la Functorclasse de type haskell .

entrez la description de l'image ici

Mais comme dans l'image ci-dessus, les fonctions F # sont séparées et implémentées seules. Quel est l'objectif de conception d'une telle conception? Pour moi, introduire Mappable.mapet implémenter cela pour chaque type de données serait plus pratique.

MyBug18
la source
Cette question n'appartient pas à SO. Ce n'est pas un problème de programmation. Je vous suggère de demander dans F # Slack ou un autre forum de discussion.
Bent Tranberg
5
@BentTranberg Généreusement lu, The community is here to help you with specific coding, algorithm, or language problems.engloberait également les questions de conception de langage, tant que les autres critères sont remplis.
Kaefer
3
Pour faire court, F # n'a pas de classes de type et doit donc être réimplémenté mapet avoir une autre fonction d'ordre supérieur commune pour chaque type de collection. Une interface n'aiderait guère car elle nécessite toujours que chaque type de collection fournisse une implémentation distincte.
dumetrulo

Réponses:

19

Oui, une question très simple à première vue. Mais si vous prenez le temps d'y réfléchir jusqu'à la fin, vous entrez dans les profondeurs de la théorie des types incommensurable. Et la théorie des types vous regarde également.

Tout d'abord, bien sûr, vous avez déjà correctement compris que F # n'a pas de classes de type, et c'est pourquoi. Mais vous proposez une interface Mappable. Ok, regardons ça.

Disons que nous pouvons déclarer une telle interface. Pouvez-vous imaginer à quoi ressemblerait sa signature?

type Mappable =
    abstract member map : ('a -> 'b) -> 'f<'a> -> 'f<'b>

fest le type implémentant l'interface. Oh, attendez! F # n'a pas ça non plus! Voici fune variable de type de type supérieur, et F # n'a pas du tout de type supérieur. Il n'y a aucun moyen de déclarer une fonction f : 'm<'a> -> 'm<'b>ou quelque chose comme ça.

Mais ok, disons que nous avons aussi surmonté cet obstacle. Et maintenant , nous avons une interface Mappablequi peut être mis en œuvre par List, Array, Seqet l'évier de la cuisine. Mais attendez! Nous avons maintenant une méthode au lieu d'une fonction, et les méthodes ne se composent pas bien! Regardons l'ajout de 42 à chaque élément d'une liste imbriquée:

// Good ol' functions:
add42 nestedList = nestedList |> List.map (List.map ((+) 42))

// Using an interface:
add42 nestedList = nestedList.map (fun l -> l.map ((+) 42))

Regardez: maintenant nous devons utiliser une expression lambda! Il n'existe aucun moyen de passer cette .mapimplémentation à une autre fonction en tant que valeur. Effectivement, la fin des "fonctions en tant que valeurs" (et oui, je sais, utiliser un lambda n'a pas l'air très mal dans cet exemple, mais croyez-moi, ça devient très moche)

Mais attendez, nous n'avons toujours pas fini. Maintenant qu'il s'agit d'un appel de méthode, l'inférence de type ne fonctionne pas! Étant donné que la signature de type d'une méthode .NET dépend du type de l'objet, le compilateur n'a aucun moyen d'inférer les deux. Il s'agit en fait d'un problème très courant pour les débutants lors de l'interopérabilité avec les bibliothèques .NET. Et le seul remède est de fournir une signature de type:

add42 (nestedList : #Mappable) = nestedList.map (fun l -> l.map ((+) 42))

Oh, mais ce n'est pas encore suffisant! Même si j'ai fourni une signature pour nestedListlui-même, je n'ai pas fourni de signature pour le paramètre lambda l. Quelle devrait être cette signature? Diriez-vous que cela devrait l'être fun (l: #Mappable) -> ...? Oh, et maintenant nous sommes finalement arrivés aux types de rang N, comme vous le voyez, #Mappableest un raccourci pour "tout type 'atel que 'a :> Mappable" - c'est-à-dire une expression lambda qui est elle-même générique.

Ou, alternativement, nous pourrions revenir à la bienveillance supérieure et déclarer le type de nestedListplus précisément:

add42 (nestedList : 'f<'a<'b>> where 'f :> Mappable, 'a :> Mappable) = ...

Mais ok, mettons de côté l'inférence de type pour l'instant et revenons à l'expression lambda et à la façon dont nous ne pouvons plus passer en maptant que valeur à une autre fonction. Disons que nous étendons un peu la syntaxe pour permettre quelque chose comme ce que fait Elm avec les champs d'enregistrement:

add42 nestedList = nestedList.map (.map ((+) 42))

Quel serait le type .map? Il faudrait que ce soit un type contraint , comme dans Haskell!

.map : Mappable 'f => ('a -> 'b) -> 'f<'a> -> 'f<'b>

Wow OK. Mis à part le fait que .NET ne permet même pas à de tels types d'exister, nous venons de récupérer les classes de types!

Mais il y a une raison pour laquelle F # n'a pas de classes de type en premier lieu. De nombreux aspects de cette raison sont décrits ci-dessus, mais une façon plus concise de le dire est: la simplicité .

Pour vous voyez, c'est une pelote de laine. Une fois que vous avez des classes de type, vous devez avoir des contraintes, une plus grande gentillesse, un rang N (ou au moins un rang 2), et avant de le savoir, vous demandez des types imprédicatifs, des fonctions de type, des GADT et tous les reste.

Mais Haskell paie un prix pour tous les cadeaux. Il s'avère qu'il n'y a aucun bon moyen de déduire tout cela. Les types de type supérieur fonctionnent, mais les contraintes ne fonctionnent pas déjà. Rang-N - n'en rêve même pas. Et même lorsque cela fonctionne, vous obtenez des erreurs de type que vous devez avoir un doctorat pour comprendre. Et c'est pourquoi dans Haskell, vous êtes gentiment encouragé à mettre des signatures de type sur tout. Eh bien, pas tout - tout , mais vraiment presque tout. Et là où vous ne mettez pas de signatures de type (par exemple à l'intérieur letet where) - surprise-surprise, ces endroits sont en fait monomorphisés, vous êtes donc essentiellement de retour dans le pays F # simpliste.

En F #, en revanche, les signatures de type sont rares, principalement pour la documentation ou pour l'interopérabilité .NET. En dehors de ces deux cas, vous pouvez écrire un gros programme complexe en F # et ne pas utiliser une seule fois une signature de type. L'inférence de type fonctionne correctement, car il n'y a rien de trop complexe ou ambigu pour qu'elle puisse être gérée.

Et c'est le gros avantage de F # sur Haskell. Oui, Haskell vous permet d'exprimer des choses super complexes d'une manière très précise, c'est bien. Mais F # vous permet d'être très délirant, presque comme Python ou Ruby, et toujours avoir le compilateur vous attraper si vous trébuchez.

Fyodor Soikin
la source