53 votes

Comment les foncteurs fonctionnent-ils en Haskell ?

Je suis en train d'apprendre Haskell et j'ai passé toutes les bases. Mais maintenant, je suis bloqué, essayant de comprendre les foncteurs.

J'ai lu que "Un foncteur transforme une catégorie en une autre catégorie". Qu'est-ce que cela signifie?

Je sais que c'est beaucoup demander, mais est-ce que quelqu'un pourrait me donner une explication en français simple des foncteurs ou peut-être un exemple d'utilisation simple?

3 votes

J'ai trouvé le billet de blog de Gabriel, Le schéma de conception du foncteur, plutôt bon. Ce n'est pas exactement de l'anglais simple, mais vous devriez le lire et voir si cela vous aide.

0 votes

126voto

AndrewC Points 21273

J'ai accidentellement écrit un

Haskell Foncteurs Tutoriel

Je vais répondre à votre question à l'aide d'exemples, et je vais mettre le types en dessous dans les commentaires.

Watch out pour le modèle dans les types de.

fmap est une généralisation de l' map

Les foncteurs sont pour vous donner l' fmap fonction. fmap fonctionne comme map, de sorte que nous allons vérifier map première:

map (subtract 1) [2,4,8,16] = [1,3,7,15]
--    Int->Int     [Int]         [Int]

Elle utilise donc la fonction (subtract 1) à l'intérieur de la liste. En fait, pour les listes, fmap fait exactement ce qu' map n'. Nous allons multiplier le tout par 10 cette fois:

fmap (* 10)  [2,4,8,16] = [10,40,80,160]
--  Int->Int    [Int]         [Int]

Je décrirais ce que la cartographie de la fonction qui multiplie par 10 sur la liste.

fmap travaille également sur l' Maybe

Quoi d'autre puis-je fmap - dessus? Nous allons Peut-être utiliser le type de données, qui dispose de deux types de valeurs, Nothing et Just x. (Vous pouvez utiliser Nothing pour représenter un échec à obtenir une réponse en Just x représente une réponse.)

fmap  (+7)    (Just 10)  = Just 17
fmap  (+7)     Nothing   = Nothing
--  Int->Int  Maybe Int    Maybe Int

OK, donc encore une fois, fmap est à l'aide de (+7) à l'intérieur de la Peut-être. Et nous pouvons fmap autres fonctions. length trouve la longueur d'une liste, de sorte que nous pouvons fmap sur Maybe [Double]

fmap    length             Nothing                      = Nothing
fmap    length    (Just [5.0, 4.0, 3.0, 2.0, 1.573458]) = Just 5
--  [Double]->Int         Maybe [Double]                  Maybe Int

En fait length :: [a] -> Int mais je l'utilise ici - [Double] je me suis donc spécialisé elle.

Nous allons utiliser show tourner des trucs dans des chaînes de caractères. Secrètement le type réel de l' show est Show a => a -> String, mais c'est un peu long, et je l'emploie ici sur un Int, de sorte qu'il est spécialisé pour Int -> String.

fmap  show     (Just 12)  = Just "12"
fmap  show      Nothing   = Nothing
-- Int->String  Maybe Int   Maybe String

aussi, en regardant en arrière à des listes

fmap   show     [3,4,5] = ["3", "4", "5"]
-- Int->String   [Int]       [String]

fmap fonctionne sur Either something

Nous allons l'utiliser sur une structure légèrement différente, Either. Les valeurs de type Either a b sont soit Left a valeurs ou Right b valeurs. Parfois, nous utiliser pour représenter un succès Right goodvalue ou de l'échec Left errordetails, et parfois juste pour mélanger les valeurs de deux types dans une. De toute façon, le foncteur pour le type de données ne fonctionne que sur l' Right - il des feuilles Left uniquement des valeurs. Qui a du sens, surtout si vous êtes en utilisant les valeurs de Droite que ceux qui réussissent (et, en fait, nous ne serions pas en mesure de le faire fonctionner sur les deux parce que les types ne sont pas nécessairement les mêmes). Permet d'utiliser le type Either String Int comme un exemple

fmap (5*)      (Left "hi")     =    Left "hi"
fmap (5*)      (Right 4)       =    Right 20
-- Int->Int  Either String Int   Either String Int

Il fait (5*) travail à l'intérieur de l'un ou l'autre, mais pour Eithers, seulement l' Right des valeurs sont modifiées. Mais nous pouvons le faire dans l'autre sens sur Either Int String, tant que la fonction travaille sur des chaînes de caractères. Mettons ", cool!" à la fin de trucs, à l'aide de (++ ", cool!").

fmap (++ ", cool!")          (Left 4)           = Left 4
fmap (++ ", cool!") (Right "fmap edits values") = Right "fmap edits values, cool!"
--   String->String    Either Int String          Either Int String

C'est cool surtout pour utiliser fmap sur IO

Maintenant, une de mes préférées des façons d'utiliser fmap est de l'utiliser sur l' IO valeurs à modifier la valeur de certains IO opération me donne. Nous allons prendre un exemple qui vous permet de taper quelque chose et puis l'imprime tout de suite:

echo1 :: IO ()
echo1 = do
    putStrLn "Say something!"
    whattheysaid <- getLine  -- getLine :: IO String
    putStrLn whattheysaid    -- putStrLn :: String -> IO ()

On peut écrire que dans une manière qui se sent plus propre à moi:

echo2 :: IO ()
echo2 = putStrLn "Say something" 
        >> getLine >>= putStrLn

>> ne fait qu'une chose après l'autre, mais la raison pour laquelle j'aime c'est parce qu' >>= prend la Chaîne getLine nous a donné et il se nourrissait de putStrLn qui prend une Chaîne de caractères. Que faire si nous voulions saluer l'utilisateur:

greet1 :: IO ()
greet1 = do
    putStrLn "What's your name?"
    name <- getLine
    putStrLn ("Hello, " ++ name)

Si nous voulions écrire que dans la plus propre façon je suis un peu coincé. J'aurais du écrire

greet2 :: IO ()
greet2 = putStrLn "What's your name?" 
         >> getLine >>= (\name -> putStrLn ("Hello, " ++ name))

qui est pas plus agréable que la do version. En fait, l' do de la notation est-il de sorte que vous n'avez pas à le faire. Mais peut - fmap venir à la rescousse? Oui, il peut. ("Hello, "++) est une fonction que je peux fmap sur le getLine!

fmap ("Hello, " ++)  getLine   = -- read a line, return "Hello, " in front of it
--   String->String  IO String    IO String

nous pouvons l'utiliser comme ceci:

greet3 :: IO ()
greet3 = putStrLn "What's your name?" 
         >> fmap ("Hello, "++) getLine >>= putStrLn

On peut tirer de ce truc sur quoi que ce soit que nous sommes donné. Nous allons être en désaccord avec le fait que "Vrai" ou "Faux" a été tapé dans:

fmap   not      readLn   = -- read a line that has a Bool on it, change it
--  Bool->Bool  IO Bool       IO Bool

Ou disons juste rapport de la taille d'un fichier:

fmap  length    (readFile "test.txt") = -- read the file, return its length
--  String->Int      IO String              IO Int
--   [a]->Int        IO [Char]              IO Int     (more precisely)

Conclusions: Qu'est - fmap le faire, et que faut-il faire pour?

Si vous avez été en regardant les modèles dans les types et la réflexion sur les exemples que vous aurez remarqué que les fmap prend une fonction qui fonctionne sur certaines valeurs, et applique cette fonction sur quelque chose qu'on a ou le produit de ces valeurs, en quelque sorte, modifier les valeurs. (par exemple readLn était de lire Bool, il a donc un type IO Bool il y a une valeur Booléenne dans le sens qu'elle produit un Bool, eg2 [4,5,6] a Ints en elle.)

fmap :: (a -> b) -> Something a -> Something b

cela fonctionne pour avoir quelque Chose de Liste de la (écrites []), Maybe, Either String, Either Int, IO et des charges de plus de choses. Nous l'appelons un Foncteur si cela fonctionne d'une manière sensible (il y a quelques règles - plus tard). Le type réel de fmap est

fmap :: Functor something => (a -> b) -> something a -> something b

mais nous avons l'habitude de remplacer something avec f pour des raisons de concision. Il est tout de même à le compilateur, si:

fmap :: Functor f => (a -> b) -> f a -> f b

Avoir un coup d'oeil sur les types et vérifier cela fonctionne toujours - chose à propos de Either String Int attentivement - ce que l' f ce moment-là?

Annexe: Quels sont le Foncteur règles, et pourquoi en avons-nous?

id est la fonction identité:

id :: a -> a
id x = x

Voici les règles:

fmap id  ==  id                    -- identity identity
fmap (f . g)  ==  fmap f . fmap g  -- composition

Tout d'abord l'identité de l'identité: Si vous avez la carte la fonction qui ne fait rien, ça ne change rien. Cela semble évident (un grand nombre de règles), mais vous pouvez l'interpréter comme disant qu' fmap est uniquement autorisé à modifier les valeurs, et non la structure. fmap n'est pas autorisé à tourner Just 4 en Nothingou [6] en [1,2,3,6]ou Right 4 en Left 4 parce que plus que la modification des données de la structure ou de contexte pour que les données modifiées.

J'ai touché à cette règle une fois quand j'ai travaillé sur une interface utilisateur graphique du projet - je voulais être en mesure de modifier les valeurs, mais je ne pouvais pas le faire sans changer la structure sous-jacente. Nul n'aurait vraiment remarqué la différence, car il a eu le même effet, mais la réalisation, il n'a pas obéir à l'foncteur règles m'a fait repenser toute ma conception, et c'est beaucoup plus propre, plus lisse et plus rapide maintenant.

Deuxièmement, la composition: cela signifie que vous pouvez choisir de fmap une seule fonction à la fois, ou fmap les deux en même temps. Si fmap quitte la structure/cadre de vos valeurs et modifie simplement avec la fonction de son, il sera de travailler avec cette règle trop.

Pourquoi en avons-nous? Assurez - fmap n'est pas sournoisement faire quoi que ce soit derrière les scènes ou de changer quoi que ce soit nous ne nous attendions pas. Ils ne sont pas appliquées par le compilateur (demander au compilateur de prouver un théorème avant qu'il compile ton code n'est pas juste, et permettrait de ralentir compilation - le programmeur doit vérifier). Cela signifie que vous pouvez tricher, mais c'est un mauvais plan parce que votre code peut donner des résultats inattendus.

0 votes

Éditer nécessite un minimum de 6 modifications, donc commenter à la place: Il y a une faute de frappe dans la sortie de fmap (* 10) où 10 devrait être 20. Bon mini-tutoriel :)

0 votes

@drumfire Merci pour ça - corrigé. :)

2 votes

Exceptionnellement bien structurée explication, tout en étant faite sans utiliser un tas d'autres termes que quelqu'un qui ne comprend pas les Foncteurs ne va pas connaître ('paramétrisée' est le principal coupable, je dirais).

63voto

Sarah Points 3390

Une explication vague serait qu'un Functor est une sorte de conteneur et une fonction associée fmap qui vous permet de modifier ce qui est contenu, en utilisant une fonction qui transforme le contenu.

Par exemple, les listes sont ce genre de conteneur, de telle sorte que fmap (+1) [1,2,3,4] donne [2,3,4,5].

Maybe peut également être un foncteur, de sorte que fmap toUpper (Just 'a') donne Just 'A'.

Le type général de fmap montre assez clairement ce qu'il se passe:

fmap :: Functor f => (a -> b) -> f a -> f b

Et les versions spécialisées peuvent rendre les choses plus claires. Voici la version pour les listes:

fmap :: (a -> b) -> [a] -> [b]

Et la version Maybe:

fmap :: (a -> b) -> Maybe a -> Maybe b

Vous pouvez obtenir des informations sur les instances standard de Functor en interrogeant GHCI avec :i Functor et de nombreux modules définissent davantage d'instances de Functor (et d'autres classes de types).

S'il vous plaît, ne prenez pas le mot "conteneur" trop au sérieux. Les Functor sont un concept bien défini, mais vous pouvez souvent raisonner à ce sujet avec cette analogie floue.

Le meilleur moyen de comprendre ce qui se passe est simplement de lire la définition de chacune des instances, ce qui devrait vous donner une intuition sur ce qui se passe. À partir de là, il n'y a qu'un petit pas à franchir pour formaliser vraiment votre compréhension du concept. Ce qu'il faut ajouter, c'est une clarification de ce que notre "conteneur" est réellement, et que chaque instance doit satisfaire une paire de lois simples.

4 votes

À partir de mon expérience, je peux dire que l'analogie d'un conteneur pour les foncteurs les rend plus difficiles à comprendre (surtout en ce qui concerne IO). Donc, au début, on peut les considérer comme une sorte de "calcul" attaché à une valeur, et non pas comme un "conteneur" contenant une valeur.

1 votes

IO lui-même n'est pas du pur Haskell. J'ai trouvé cette explication plus claire: Considérons putStrLn :: String -> IO (). Cette fonction prend un String et retourne un IO (), qui peut être vu comme une computation, dans ce cas une sortie vers stdout. L'explication basée sur le conteneur n'est pas aussi claire que l'explication basée sur la computation. Mais ce n'est que mon opinion.

2 votes

@WillNess @Anton a raison. L'analogie du conteneur ne peut être utilisée que pour simplifier. Cependant, elle n'est pas du tout bien définie. Considérons par exemple le foncteur Const a :)

15voto

Ben Points 22160

Il est important de garder à l'esprit la distinction entre un foncteur lui-même et une valeur dans un type auquel un foncteur est appliqué. Un foncteur lui-même est un constructeur de type comme Maybe, IO, ou le constructeur de liste []. Une valeur dans un foncteur est une valeur particulière dans un type avec ce constructeur de type appliqué. par exemple, Just 3 est une valeur particulière dans le type Maybe Int (ce type est le foncteur Maybe appliqué au type Int), putStrLn "Hello World" est une valeur particulière dans le type IO (), et [2, 4, 8, 16, 32] est une valeur particulière dans le type [Int].

J'aime penser à une valeur dans un type avec un foncteur appliqué comme étant "la même" qu'une valeur dans le type de base, mais avec un "contexte" supplémentaire. Les gens utilisent souvent une analogie de conteneur pour un foncteur, ce qui fonctionne assez naturellement pour beaucoup de foncteurs, mais qui devient ensuite plus un obstacle qu'une aide lorsque vous devez vous convaincre que IO ou (->) r est comme un conteneur.

Donc, si un Int représente une valeur entière, alors un Maybe Int représente une valeur entière qui peut ne pas être présente ("peut ne pas être présente" est le "contexte"). Un [Int] représente une valeur entière avec un certain nombre de valeurs possibles (c'est la même interprétation du foncteur de liste que l'interprétation "non déterministe" du monade de liste). Un IO Int représente une valeur entière dont la valeur précise dépend de l'univers entier (ou alternativement, il représente une valeur entière qui peut être obtenue en exécutant un processus externe). Un Char -> Int est une valeur entière pour n'importe quelle valeur Char ("fonction prenant r comme argument" est un foncteur pour n'importe quel type r; avec r comme Char (->) Char est le constructeur de type qui est un foncteur, qui appliqué à Int devient (->) Char Int ou Char -> Int en notation infix).

La seule chose que vous pouvez faire avec un foncteur général est fmap, avec le type Functor f => (a -> b) -> (f a -> f b). fmap transforme une fonction qui opère sur des valeurs normales en une fonction qui opère sur des valeurs auxquelles un contexte supplémentaire est ajouté par un foncteur; ce que cela fait exactement est différent pour chaque foncteur, mais vous pouvez le faire avec tous.

Ainsi, avec le foncteur Maybe, fmap (+1) est la fonction qui calcule éventuellement un entier plus élevé de 1 que son entrée d'entier éventuellement non présente. Avec le foncteur de liste fmap (+1) est la fonction qui calcule un entier non déterministe plus élevé de 1 que son entier non déterministe d'entrée. Avec le foncteur IO, fmap (+1) est la fonction qui calcule un entier plus élevé de 1 que son entier-dont-la-valeur-dépend-de-l'univers-externe. Avec le foncteur (->) Char, fmap (+1) est la fonction qui ajoute 1 à un entier qui dépend d'un Char (quand je donne un Char à la valeur de retour, j'obtiens 1 de plus que ce que j'aurais obtenu en donnant le même Char à la valeur d'origine).

Mais en général, pour un foncteur inconnu f, fmap (+1) appliquée à une valeur dans f Int est la version "foncteur" de la fonction (+1) sur des Int ordinaires. Il ajoute 1 à l'entier dans le "contexte" de ce foncteur particulier.

En soi, fmap n'est pas nécessairement très utile. Habituellement, lorsque vous écrivez un programme concret et que vous travaillez avec un foncteur, vous travaillez avec un foncteur particulier, et vous pensez souvent à fmap comme à ce qu'il fait pour ce foncteur particulier. Lorsque je travaille avec des [Int], je ne pense souvent pas à mes valeurs [Int] comme des entiers non déterministes, je les pense juste comme des listes de entiers, et je pense à fmap de la même manière que je pense à map.

Alors pourquoi s'embêter avec des foncteurs? Pourquoi ne pas avoir simplement map pour les listes, applyToMaybe pour les Maybe, et applyToIO pour les IO? Alors tout le monde saurait ce qu'ils font et personne ne devrait comprendre des concepts abstraits étranges comme les foncteurs.

La clé est de reconnaître qu'il y a beaucoup de foncteurs là-bas; presque tous les types conteneurs pour commencer (d'où l'analogie de conteneur pour ce que sont les foncteurs). Chacun d'entre eux a une opération correspondant à fmap, même si nous n'avons pas de foncteurs. Chaque fois que vous écrivez un algorithme uniquement en terme de l'opération fmap (ou map, ou quel que soit son nom pour votre type particulier), alors si vous l'écrivez en termes de foncteurs plutôt que de votre type particulier alors il fonctionne pour tous les foncteurs.

Cela peut également servir de forme de documentation. Si je transmets l'une de mes valeurs de liste à une fonction que vous avez écrite qui opère sur des listes, elle pourrait faire un certain nombre de choses. Mais si je transmets ma liste à une fonction que vous avez écrite qui opère sur des valeurs dans un foncteur arbitraire, alors je sais que la mise en œuvre de votre fonction ne peut pas utiliser des fonctionnalités de liste, seulement des fonctionnalités de foncteur.

Penser à la façon dont vous utiliseriez des choses qui ressemblent à des foncteurs dans la programmation impérative traditionnelle peut aider à voir les avantages. Là-bas, des types de conteneurs comme les tableaux, les listes, les arbres, etc., auront généralement un motif que vous utilisez pour les parcourir. Il peut être légèrement différent pour différents conteneurs, bien que les bibliothèques fournissent souvent des interfaces d'itération standard pour y remédier. Mais vous finissez toujours par écrire une petite boucle for à chaque fois que vous voulez les parcourir, et lorsque ce que vous voulez faire est de calculer un résultat pour chaque élément du conteneur et d'obtenir tous les résultats, vous finissez généralement par mélanger la logique de construction du nouveau conteneur au fur et à mesure que vous avancez.

fmap est toutes les boucles for de cette forme que vous écrirez jamais, triées une fois pour toutes par les écrivains de bibliothèques, avant même que vous ne vous asseyiez pour programmer. De plus, il peut également être utilisé avec des choses comme Maybe et (->) r qui ne seraient probablement pas vues comme ayant quoi que ce soit à voir avec la conception d'une interface de conteneur cohérente dans les langages impératifs.

8voto

hammar Points 89293

En Haskell, les foncteurs capturent la notion d'avoir des conteneurs de "choses", de sorte que vous puissiez manipuler cette "chose" sans changer la forme du conteneur.

Les foncteurs fournissent une fonction, fmap, qui vous permet de faire cela, en prenant une fonction régulière et en la "élevant" à une fonction d'un conteneur d'un type d'élément à un autre :

fmap :: Functor f => (a -> b) -> (f a -> f b) 

Par exemple, [], le constructeur de type de liste, est un foncteur :

> fmap show [1, 2, 3]
["1","2","3"]

et de même pour de nombreux autres constructeurs de types Haskell, comme Maybe et Map Integer1 :

> fmap (+1) (Just 3)
Just 4
> fmap length (Data.Map.fromList [(1, "hi"), (2, "there")])
fromList [(1,2),(2,5)]

Remarquez que fmap n'est pas autorisée à changer la "forme" du conteneur, donc si par exemple vous fmap une liste, le résultat a le même nombre d'éléments, et si vous fmap un Just, il ne peut pas devenir un Nothing. En termes formels, nous exigeons que fmap id = id, c'est-à-dire si vous fmap la fonction d'identité, rien ne change.

Jusqu'à présent, j'ai utilisé le terme "conteneur", mais c'est en fait un peu plus général que ça. Par exemple, IO est aussi un foncteur, et ce que nous entendons par "forme" dans ce cas, c'est que fmap sur une action IO ne devrait pas changer les effets secondaires. En fait, tout monade est un foncteur2.

En théorie des catégories, les foncteurs vous permettent de convertir entre différentes catégories, mais en Haskell, nous n'avons vraiment qu'une seule catégorie, souvent appelée Hask. Par conséquent, tous les foncteurs en Haskell convertissent de Hask à Hask, ce sont donc ce que nous appelons des endofoncteurs (foncteurs d'une catégorie vers elle-même).

Dans leur forme la plus simple, les foncteurs sont assez ennuyeux. Il n'y a vraiment que tant de choses que vous pouvez faire avec une seule opération. Cependant, une fois que vous commencez à ajouter des opérations, vous pouvez passer des foncteurs réguliers aux foncteurs applicatifs aux monades et les choses deviennent rapidement beaucoup plus intéressantes, mais c'est au-delà de la portée de cette réponse.

<sup>1</sup> Mais <code>Set</code> ne l'est pas, car il ne peut stocker que des types <code>Ord</code>. Les foncteurs doivent pouvoir contenir n'importe quel type.
<sup>2</sup> Pour des raisons historiques, <code>Functor</code> n'est pas une superclasse de <code>Monad</code>, bien que beaucoup de gens pensent que cela devrait l'être.

4voto

Will Ness Points 18581

Regardons les types.

Prelude> :i Functor
class Functor f where fmap :: (a -> b) -> f a -> f b

Mais que cela signifie-t-il?

D'abord, f est une variable de type ici, et elle représente un constructeur de type: f a est un type; a est une variable de type représentant un type spécifique.

Ensuite, étant donnée une fonction g :: a -> b, vous obtiendrez fmap g :: f a -> f b. C'est-à-dire que fmap g est une fonction, transformant des éléments de type f a en des éléments de type f b. Remarquez que nous ne pouvons pas accéder aux éléments de type a ni bg :: a -> b est de quelque manière faite pour agir sur des éléments de type f a et les transformer en des éléments de type f b.

Remarquez que f reste le même. Seul l'autre type change.

Que signifie cela? Cela peut signifier beaucoup de choses. f est généralement vu comme un "conteneur" de choses. Ensuite, fmap g permet à g d'agir à l'intérieur de ces conteneurs, sans les ouvrir. Les résultats sont toujours enfermés à l'intérieur, la classe de type Functor ne nous donne pas la capacité de les ouvrir ou de regarder à l'intérieur. Nous obtenons simplement une transformation à l'intérieur de choses opaques. Toute autre fonctionnalité devra provenir d'ailleurs.

Remarquez également qu'il n'est pas dit que ces "conteneurs" transportent juste une "chose" de type a; il peut y avoir plusieurs "choses" distinctes "à l'intérieur", mais toutes du même type a.

Enfin, tout candidat pour un foncteur doit obéir aux lois du Foncteur:

fmap id      ===  id
fmap (h . g) ===  fmap h . fmap g

Remarquez que les types des deux opérateurs (.) sont différents:

     g  :: a -> b                         fmap g  :: f a -> f b
 h      ::      b -> c           fmap h           ::        f b -> f c
----------------------          --------------------------------------
(h . g) :: a      -> c          (fmap h . fmap g) :: f a        -> f c

Cela signifie que quelle que soit la relation qui existe entre les types a, b et c en "connectant les fils" pour ainsi dire des fonctions comme g et h, cette relation existe également entre les types f a, f b et f c en connectant les fils des fonctions fmap g et fmap h.

Ou, n'importe quel diagramme connecté "du côté gauche", dans le monde a, b, c, ..., peut être dessiné "du côté droit", dans le monde f a, f b, f c, ... en changeant les fonctions g, h, ... en les fonctions fmap g, fmap h, ..., et en changeant les fonctions id :: a -> a en fmap id qui sont elles-mêmes juste id :: f a -> f a, selon les lois du Foncteur.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X