Buoni esempi di Not a Functor / Functor / Applicative / Monad?


210

Mentre spiego a qualcuno cos'è una classe di tipo X, faccio fatica a trovare buoni esempi di strutture dati che siano esattamente X.

Quindi, chiedo esempi per:

  • Un costruttore di tipi che non è un Functor.
  • Un costruttore di tipi che è un Functor, ma non Applicativo.
  • Un costruttore di tipi che è un applicativo, ma non è una monade.
  • Un costruttore di tipi che è una Monade.

Penso che ci siano molti esempi di Monade ovunque, ma un buon esempio di Monade con qualche relazione con gli esempi precedenti potrebbe completare il quadro.

Cerco esempi che siano simili tra loro, differendo solo per aspetti importanti per l'appartenenza alla particolare classe di tipo.

Se si potesse riuscire a cogliere di soppiatto un esempio di Arrow da qualche parte in questa gerarchia (è tra Applicative e Monad?), Anche quello sarebbe fantastico!


4
E 'possibile fare un tipo di costruzione ( * -> *) per la quale non esiste alcun adatto fmap?
Owen

1
Owen, penso che a -> Stringnon sia un funtore.
Rotsor

3
@Rotsor @Owen a -> Stringè un funtore matematico, ma non un Haskell Functor, per essere chiari.
J. Abrahamson

@J. Abrahamson, in che senso è allora un funtore matematico? Stai parlando della categoria con le frecce invertite?
Rotsor

5
Per le persone che non lo sanno, un funtore controvariante ha una mappa di tipo(a -> b) -> f b -> f a
AJF

Risposte:


100

Un costruttore di tipi che non è un Functor:

newtype T a = T (a -> Int)

Puoi ricavarne un funtore controvariante, ma non un funtore (covariante). Prova a scrivere fmape fallirai. Si noti che la versione del funtore controvariante è invertita:

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

Un costruttore di tipi che è un funtore, ma non applicativo:

Non ho un buon esempio. C'è Const, ma idealmente vorrei un calcestruzzo non Monoide e non riesco a pensare di qualsiasi. Tutti i tipi sono fondamentalmente numerici, enumerazioni, prodotti, somme o funzioni quando ci si arriva. Puoi vedere di seguito io e il pigworker in disaccordo sul fatto che Data.Voidsia un Monoid;

instance Monoid Data.Void where
    mempty = undefined
    mappend _ _ = undefined
    mconcat _ = undefined

Poiché _|_è un valore legale in Haskell, e in effetti l'unico valore legale di Data.Void, questo soddisfa le regole Monoid. Non sono sicuro di cosa c'entri unsafeCoerce, perché il tuo programma non è più garantito per non violare la semantica Haskell non appena usi una unsafefunzione.

Vedere Haskell Wiki per un articolo sulla parte inferiore ( collegamento ) o sulle funzioni non sicure ( collegamento ).

Mi chiedo se sia possibile creare un tale costruttore di tipi utilizzando un sistema di tipi più ricco, come Agda o Haskell con varie estensioni.

Un costruttore di tipi che è un applicativo, ma non una monade:

newtype T a = T {multidimensional array of a}

Puoi farne un applicativo, con qualcosa del tipo:

mkarray [(+10), (+100), id] <*> mkarray [1, 2]
  == mkarray [[11, 101, 1], [12, 102, 2]]

Ma se la trasformi in una monade, potresti ottenere una mancata corrispondenza delle dimensioni. Sospetto che esempi come questo siano rari nella pratica.

Un costruttore di tipi che è una Monade:

[]

Informazioni sulle frecce:

Chiedere dove si trova una Freccia in questa gerarchia è come chiedere che tipo di forma sia "rossa". Nota il tipo di mancata corrispondenza:

Functor :: * -> *
Applicative :: * -> *
Monad :: * -> *

ma,

Arrow :: * -> * -> *

3
Buona lista! Suggerirei di utilizzare qualcosa di più semplice come Either aesempio per l'ultimo caso, poiché è più facile da capire.
fuz

6
Se stai ancora cercando un costruttore di tipi che sia Applicativo ma non una Monade, un esempio molto comune sarebbe ZipList.
John L

23
_|_abita ogni tipo in *, ma il punto Voidè che dovresti piegarti all'indietro per costruirne uno o hai distrutto il suo valore. Questo è il motivo per cui non è un'istanza di Enum, Monoid, ecc. Se ne hai già uno, sono felice di permetterti di mescolarli insieme (dandoti un Semigroup) ma mempty, ma non do strumenti per costruire esplicitamente un valore di tipo Voidin void. Devi caricare la pistola e puntarla verso il tuo piede e premere il grilletto da solo.
Edward KMETT

2
Pedanticamente, penso che la tua idea di Cofunctor sia sbagliata. Il duale di un funtore è un funtore, perché capovolgi sia l'input che l'output e finisci con la stessa cosa. La nozione che stai cercando è probabilmente "funtore controvariante", che è leggermente diversa.
Ben Millwood

1
@AlexVong: "Deprecated" -> le persone usano solo un pacchetto diverso. Parlando di "funtore controvariante" non "duale di funtore", ci scusiamo per la confusione. In alcuni contesti ho visto "cofunctor" usato per riferirsi a "funtori controvarianti" perché i funtori sono auto-duali, ma sembra essere solo gente che confonde.
Dietrich Epp

86

Il mio stile potrebbe essere limitato dal mio telefono, ma ecco qua.

newtype Not x = Kill {kill :: x -> Void}

non può essere un functor. Se lo fosse, avremmo

kill (fmap (const ()) (Kill id)) () :: Void

e la luna sarebbe stata fatta di formaggio verde.

Nel frattempo

newtype Dead x = Oops {oops :: Void}

è un funtore

instance Functor Dead where
  fmap f (Oops corpse) = Oops corpse

ma non può essere applicativo, altrimenti avremmo

oops (pure ()) :: Void

e il verde sarebbe fatto di formaggio Moon (cosa che può effettivamente accadere, ma solo più tardi la sera).

(Nota extra: Voidcome in Data.Voidè un tipo di dati vuoto. Se provi a usare undefinedper dimostrare che è un Monoid, lo userò unsafeCoerceper dimostrare che non lo è.)

Gioiosamente,

newtype Boo x = Boo {boo :: Bool}

è applicativo in molti modi, ad esempio, come vorrebbe Dijkstra,

instance Applicative Boo where
  pure _ = Boo True
  Boo b1 <*> Boo b2 = Boo (b1 == b2)

ma non può essere una Monade. Per vedere perché no, osserva che il ritorno deve essere costantemente Boo Trueo Boo False, e quindi quello

join . return == id

non può reggere.

Oh sì, quasi dimenticavo

newtype Thud x = The {only :: ()}

è una monade. Rotola il tuo.

Aereo da prendere ...


8
Void è vuoto! Moralmente, comunque.
operaio suino

9
Void è un tipo con 0 costruttori, presumo. Non è un monoide perché non esiste mempty.
Rotsor

6
non definito? Che maleducato! Purtroppo, unsafeCoerce (unsafeCoerce () <*> undefined) non è (), quindi nella vita reale ci sono osservazioni che violano le leggi.
Pigworker

5
Nella solita semantica, che tollera esattamente un tipo di indefinito, hai ragione. Ci sono altre semantiche, ovviamente. Void non si limita a un sottomonoide nel frammento totale. Né è un monoide in una semantica che distingue le modalità di fallimento. Quando ho un momento con l'editing più facile rispetto a quello telefonico, chiarirò che il mio esempio funziona solo in una semantica per la quale non esiste esattamente un tipo di indefinito.
Pigworker

23
Molto rumore per_|_
Landei

72

Credo che le altre risposte abbiano mancato alcuni esempi semplici e comuni:

Un costruttore di tipi che è un Functor ma non un applicativo. Un semplice esempio è una coppia:

instance Functor ((,) r) where
    fmap f (x,y) = (x, f y)

Ma non c'è modo di definire la sua Applicativeistanza senza imporre ulteriori restrizioni su r. In particolare, non c'è modo di definire pure :: a -> (r, a)per un arbitrario r.

Un costruttore di tipi che è un applicativo, ma non è una monade. Un esempio noto è ZipList . (È un newtypeche avvolge gli elenchi e fornisce loro Applicativeistanze diverse .)

fmapè definito nel solito modo. Ma puree <*>sono definiti come

pure x                    = ZipList (repeat x)
ZipList fs <*> ZipList xs = ZipList (zipWith id fs xs)

quindi purecrea un elenco infinito ripetendo il valore dato e <*>comprime un elenco di funzioni con un elenco di valori - applica i -esima funzione all'i -esimo elemento. (Lo standard <*>su []produce tutte le possibili combinazioni di applicare i funzione -esimo al j elemento -esimo.), Ma non c'è modo sensibile come definire una monade (vedi questo post ).


Come si inseriscono le frecce nella gerarchia funtore / applicativa / monade? Vedi Idiomi sono ignari, le frecce sono meticolose, le monadi sono promiscue di Sam Lindley, Philip Wadler, Jeremy Yallop. MSFP 2008. (Chiamano idiomi dei funtori applicativi ). L'abstract:

Rivediamo la connessione tra tre nozioni di calcolo: le monadi di Moggi, le frecce di Hughes e gli idiomi di McBride e Paterson (chiamati anche funtori applicativi). Mostriamo che gli idiomi sono equivalenti alle frecce che soddisfano il tipo di isomorfismo A ~> B = 1 ~> (A -> B) e che le monadi sono equivalenti alle frecce che soddisfano il tipo di isomorfismo A ~> B = A -> (1 ~ > B). Inoltre, gli idiomi si incorporano nelle frecce e le frecce si incorporano nelle monadi.


1
Quindi ((,) r)è un funtore che non è un applicativo; ma questo è solo perché generalmente non puoi definire puretutto rin una volta. È quindi un capriccio della concisione del linguaggio, del tentativo di definire una collezione (infinita) di funtori applicativi con una definizione di puree <*>; in questo senso, non sembra esserci nulla di matematicamente profondo in questo controesempio poiché, per qualsiasi concreto r, ((,) r) può essere reso un funtore applicativo. Domanda: Riesci a pensare a un funtore CONCRETO che non sia un applicativo?
George,

1
Vedi stackoverflow.com/questions/44125484/… come post con questa domanda.
George,

21

Un buon esempio per un costruttore di tipi che non è un funtore è Set: non puoi implementarlo fmap :: (a -> b) -> f a -> f b, perché senza un vincolo aggiuntivo Ord bnon puoi costruire f b.


16
In realtà è un buon esempio poiché matematicamente vorremmo davvero renderlo un funtore.
Alexandre C.

21
@AlexandreC. Non sono d'accordo su questo, non è un buon esempio. Matematicamente, una tale struttura dati forma un funtore. Il fatto che non possiamo implementare fmapè solo un problema di lingua / implementazione. Inoltre, è possibile Setentrare nella monade di continuazione, che ne fa una monade con tutte le proprietà che ci aspetteremmo, vedi questa domanda (anche se non sono sicuro che possa essere fatto in modo efficiente).
Petr Pudlák

@PetrPudlak, com'è questo un problema di lingua? L'uguaglianza di bpuò essere indecidibile, in quel caso non puoi definirla fmap!
Turion

@Turion Essere decidibile e definibile sono due cose diverse. Ad esempio è possibile definire correttamente l'uguaglianza sui termini lambda (programmi), anche se non è possibile deciderla con un algoritmo. In ogni caso, questo non è stato il caso di questo esempio. Qui il problema è che non possiamo definire Functorun'istanza con il Ordvincolo, ma potrebbe essere possibile con una diversa definizione Functoro un migliore supporto linguistico. In realtà con ConstraintKinds è possibile definire una classe di tipo che può essere parametrizzata in questo modo.
Petr Pudlák

Anche se potessimo superare il ordvincolo, il fatto che a Setnon possa contenere voci duplicate significa che fmappotrebbe alterare il contesto. Ciò viola la legge sull'associatività.
John F. Miller

12

Vorrei proporre un approccio più sistematico per rispondere a questa domanda e anche mostrare esempi che non utilizzano trucchi speciali come i valori "inferiori" o tipi di dati infiniti o qualcosa del genere.

Quando i costruttori di tipi non riescono ad avere istanze di classi di tipo?

In generale, ci sono due ragioni per cui un costruttore di tipi potrebbe non riuscire ad avere un'istanza di una determinata classe di tipo:

  1. Impossibile implementare le firme del tipo dei metodi richiesti dalla classe del tipo.
  2. Può implementare le firme del tipo ma non può soddisfare le leggi richieste.

Gli esempi del primo tipo sono più facili di quelli del secondo tipo perché per il primo tipo, dobbiamo solo verificare se si può implementare una funzione con una data firma del tipo, mentre per il secondo tipo, dobbiamo dimostrare che nessuna implementazione potrebbe eventualmente soddisfare le leggi.

Esempi specifici

  • Un costruttore di tipi che non può avere un'istanza del funtore perché il tipo non può essere implementato:

    data F z a = F (a -> z)
    

Questo è un controfuntore, non un funtore, rispetto al parametro di tipo a, perché ain posizione controvariante. È impossibile implementare una funzione con la firma del tipo (a -> b) -> F z a -> F z b.

  • Un costruttore di tipi che non è un funtore legale anche se la firma del tipo di fmappuò essere implementata:

    data Q a = Q(a -> Int, a)
    fmap :: (a -> b) -> Q a -> Q b
    fmap f (Q(g, x)) = Q(\_ -> g x, f x)  -- this fails the functor laws!
    

L'aspetto curioso di questo esempio è che si può implementare fmapdel tipo corretto, anche se Fnon può possibilmente essere un funtore perché utilizza ain una posizione controvariante. Quindi questa implementazione di quella fmapmostrata sopra è fuorviante - anche se ha la firma di tipo corretta (credo che questa sia l'unica implementazione possibile di quella firma di tipo), le leggi del funtore non sono soddisfatte. Ad esempio, fmap idid, perché let (Q(f,_)) = fmap id (Q(read,"123")) in f "456"è 123, ma let (Q(f,_)) = id (Q(read,"123")) in f "456"è 456.

In realtà, Fè solo un profunctor, - non è né un funtore né un controfuntore.

  • Un funtore legale che non è applicativo perché la firma del tipo di purenon può essere implementata: prendi la monade Writer (a, w)e rimuovi il vincolo che wdovrebbe essere un monoide. È quindi impossibile costruire un valore di tipo (a, w)da a.

  • Un funtore che non è applicativa perché la firma di tipo <*>non può essere attuata: data F a = Either (Int -> a) (String -> a).

  • Un funtore che non è applicativo legale anche se i metodi della classe di tipo possono essere implementati:

    data P a = P ((a -> Int) -> Maybe a)
    

Il costruttore del tipo Pè un funtore perché utilizza asolo in posizioni covarianti.

instance Functor P where
   fmap :: (a -> b) -> P a -> P b
   fmap fab (P pa) = P (\q -> fmap fab $ pa (q . fab))

L'unica possibile implementazione della firma del tipo di <*>è una funzione che restituisce sempre Nothing:

 (<*>) :: P (a -> b) -> P a -> P b
 (P pfab) <*> (P pa) = \_ -> Nothing  -- fails the laws!

Ma questa implementazione non soddisfa la legge sull'identità per i funtori applicativi.

  • Un funtore che è Applicativema nonMonad perché la firma del tipo di bindnon può essere implementata.

Non conosco esempi del genere!

  • Un funtore che è Applicativema non unMonad perché le leggi non possono essere soddisfatte anche se la firma del tipo di bindpuò essere implementata.

Questo esempio ha generato un bel po 'di discussioni, quindi è sicuro dire che dimostrare che questo esempio è corretto non è facile. Ma molte persone lo hanno verificato in modo indipendente con metodi diversi. Vedere È `data PoE a = Empty | Associare aa` una monade? per ulteriori discussioni.

 data B a = Maybe (a, a)
   deriving Functor

 instance Applicative B where
   pure x = Just (x, x)
   b1 <*> b2 = case (b1, b2) of
     (Just (x1, y1), Just (x2, y2)) -> Just((x1, x2), (y1, y2))
     _ -> Nothing

È piuttosto complicato dimostrare che non esiste Monadun'istanza legittima . La ragione del comportamento non monadico è che non esiste un modo naturale di implementare bindquando una funzione f :: a -> B bpotrebbe restituire Nothingo Justper valori diversi di a.

È forse più chiaro considerare Maybe (a, a, a), che anche non è una monade, e provare a implementare joinper questo. Si scoprirà che non esiste un modo di implementazione intuitivamente ragionevole join.

 join :: Maybe (Maybe (a, a, a), Maybe (a, a, a), Maybe (a, a, a)) -> Maybe (a, a, a)
 join Nothing = Nothing
 join Just (Nothing, Just (x1,x2,x3), Just (y1,y2,y3)) = ???
 join Just (Just (x1,x2,x3), Nothing, Just (y1,y2,y3)) = ???
 -- etc.

Nei casi indicati da ???, sembra chiaro che non possiamo produrre Just (z1, z2, z3)in modo ragionevole e simmetrico su sei diversi valori di tipo a. Potremmo certamente scegliere qualche sottoinsieme arbitrario di questi sei valori, - per esempio, prendere sempre il primo non vuoto Maybe- ma questo non soddisferebbe le leggi della monade. Il ritorno Nothinginoltre non soddisferà le leggi.

  • Una struttura dati ad albero che non è una monade anche se ha associatività per bind- ma non soddisfa le leggi sull'identità.

La solita monade ad albero (o "un albero con rami a forma di funtore") è definita come

 data Tr f a = Leaf a | Branch (f (Tr f a))

Questa è una monade libera sul funtore f. La forma dei dati è un albero in cui ogni punto di diramazione è un "funtore" di sottoalberi. L'albero binario standard sarebbe ottenuto con type f a = (a, a).

Se modifichiamo questa struttura di dati facendo anche le foglie a forma di funtore f, otteniamo quello che chiamo un "semimonad" - ha bindche soddisfa le leggi di naturalità e associatività, ma il suo puremetodo fallisce una delle leggi di identità. "I semimonad sono semigruppi nella categoria degli endofunctors, qual è il problema?" Questa è la classe del tipo Bind.

Per semplicità definisco il joinmetodo invece di bind:

 data Trs f a = Leaf (f a) | Branch (f (Trs f a))
 join :: Trs f (Trs f a) -> Trs f a
 join (Leaf ftrs) = Branch ftrs
 join (Branch ftrstrs) = Branch (fmap @f join ftrstrs)

L'innesto del ramo è standard, ma l'innesto fogliare non è standard e produce a Branch. Questo non è un problema per la legge sull'associatività, ma infrange una delle leggi sull'identità.

Quando i tipi polinomiali hanno istanze di monade?

Nessuno dei funtori Maybe (a, a)e Maybe (a, a, a)può ricevere Monadun'istanza legittima , sebbene lo siano ovviamente Applicative.

Questi funtori non hanno trucchi: niente Voido da bottomnessuna parte, nessuna pigrizia / rigore complicato, nessuna struttura infinita e nessun tipo di vincoli di classe. L' Applicativeistanza è completamente standard. Le funzioni returne bindpossono essere implementate per questi funtori ma non soddisfano le leggi della monade. In altre parole, questi funtori non sono monadi perché manca una struttura specifica (ma non è facile capire cosa manchi esattamente). Ad esempio, un piccolo cambiamento nel funtore può trasformarlo in una monade: data Maybe a = Nothing | Just aè una monade. Un altro funtore simile data P12 a = Either a (a, a)è anche una monade.

Costruzioni per monadi polinomiali

In generale, ecco alcune costruzioni che producono Monads legali da tipi polinomiali. In tutte queste costruzioni, Mè una monade:

  1. type M a = Either c (w, a)dov'è wun monoide
  2. type M a = m (Either c (w, a))dove mè qualsiasi monade ed wè qualsiasi monoide
  3. type M a = (m1 a, m2 a)dove m1e m2sono tutte le monadi
  4. type M a = Either a (m a)dov'è muna monade

La prima costruzione è WriterT w (Either c), la seconda è WriterT w (EitherT c m). La terza costruzione è un prodotto per componente delle monadi: pure @Mè definita come il prodotto per componente di pure @m1e pure @m2, ed join @Mè definita omettendo i dati del prodotto incrociato (ad esempio, m1 (m1 a, m2 a)è mappata m1 (m1 a)omettendo la seconda parte della tupla):

 join :: (m1 (m1 a, m2 a), m2 (m1 a, m2 a)) -> (m1 a, m2 a)
 join (m1x, m2x) = (join @m1 (fmap fst m1x), join @m2 (fmap snd m2x))

La quarta costruzione è definita come

 data M m a = Either a (m a)
 instance Monad m => Monad M m where
    pure x = Left x
    join :: Either (M m a) (m (M m a)) -> M m a
    join (Left mma) = mma
    join (Right me) = Right $ join @m $ fmap @m squash me where
      squash :: M m a -> m a
      squash (Left x) = pure @m x
      squash (Right ma) = ma

Ho verificato che tutte e quattro le costruzioni producano monadi legali.

I Ipotizzo che non ci sono altre costruzioni per monadi polinomi. Ad esempio, il funtore Maybe (Either (a, a) (a, a, a, a))non si ottiene attraverso nessuna di queste costruzioni e quindi non è monadico. Tuttavia, Either (a, a) (a, a, a)è monade perché è isomorfo al prodotto di tre monadi a, a, e Maybe a. Inoltre, Either (a,a) (a,a,a,a)è monadico perché è isomorfo al prodotto di ae Either a (a, a, a).

Le quattro costruzioni mostrate sopra ci permetteranno di ottenere qualsiasi somma di qualsiasi numero di prodotti di qualsiasi numero di a, per esempio, Either (Either (a, a) (a, a, a, a)) (a, a, a, a, a))e così via. Tutti questi costruttori di tipi avranno (almeno una) Monadistanza.

Resta da vedere, ovviamente, quali casi d'uso potrebbero esistere per tali monadi. Un altro problema è che le Monadistanze derivate tramite le costruzioni 1-4 in generale non sono uniche. Ad esempio, il costruttore del tipo type F a = Either a (a, a)può ricevere Monadun'istanza in due modi: dalla costruzione 4 usando la monade (a, a)e dalla costruzione 3 usando l'isomorfismo del tipo Either a (a, a) = (a, Maybe a). Ancora una volta, trovare casi d'uso per queste implementazioni non è immediatamente ovvio.

Rimane una domanda: dato un tipo di dati polinomiale arbitrario, come riconoscere se ha Monadun'istanza. Non so come dimostrare che non ci siano altre costruzioni per le monadi polinomiali. Non credo che finora esista alcuna teoria per rispondere a questa domanda.


Penso B sia una monade. Puoi dare un controesempio a questo legame Pair x y >>= f = case (f x, f y) of (Pair x' _,Pair _ y') -> Pair x' y' ; _ -> Empty?
Franky

@Franky Associativity fallisce con questa definizione quando si sceglie ftale che f xè Emptyma f yè a Pair, e nel passaggio successivo lo sono entrambi Pair. Ho verificato a mano che le leggi non valgono per questa implementazione o per qualsiasi altra implementazione. Ma è un bel po 'di lavoro per farlo. Vorrei che ci fosse un modo più semplice per capirlo!
winitzki

1
@Turion Questo argomento non si applica a Maybeperché Maybenon contiene valori diversi di adi cui preoccuparsi.
Daniel Wagner

1
@ Turion l'ho dimostrato con un paio di pagine di calcoli; l'argomento sulla "via naturale" è solo una spiegazione euristica. Un Monadesempio è costituito da funzioni returne bindche le leggi Satisfy. Ci sono due implementazioni returne 25 implementazioni di bindquelle adatte ai tipi richiesti. Puoi dimostrare con un calcolo diretto che nessuna delle implementazioni soddisfa le leggi. Per ridurre la quantità di lavoro necessaria, ho usato joinal posto di binde usato le leggi di identità prima. Ma è stato un bel po 'di lavoro.
winitzki

1
@duplode No, non credo Traversablesia necessario. m (Either a (m a))viene trasformato utilizzando pure @min m (Either (m a) (m a)). Quindi banalmente Either (m a) (m a) -> m a, e possiamo usare join @m. Quella era l'implementazione per la quale ho controllato le leggi.
winitzki
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.