Bene, consideriamo i modi in cui qualcosa come questo potrebbe essere essere semplificata. Una versione non monadica potrebbe sembrare qualcosa come const' f a = const a (f a)
, che è chiaramente equivalente a flip const
con un tipo più specifico. Con la versione monadica, tuttavia, il risultato di f a
può fare cose arbitrarie alla struttura non parametrica del funtore (ad esempio, quelli che vengono spesso chiamati "effetti collaterali"), comprese le cose che dipendono dal valore di a
. Ciò che questo ci dice è che, nonostante fingiamo di avere un come se stessimo scartando il risultato di f a
, in realtà non stiamo facendo nulla del genere. Restituisce a
invariato poiché la parte parametrica del functor è molto meno essenziale e potremmo sostituire return
con qualcos'altro e avere ancora una funzione concettualmente simile.
Quindi la prima cosa che possiamo concludere è che può essere visto come un caso particolare di una funzione come la seguente:
doBoth :: (Monad m) => (a -> m b) -> (a -> m c) -> a -> m c
doBoth f g a = f a >> g a
Da qui, ci sono due modi diversi di guardare per una struttura di fondo di qualche tipo.
Una prospettiva è di riconoscere lo schema di frazionamento un singolo argomento tra più funzioni, poi ricombinare i risultati. Questo è il concetto incarnato dai Applicative
/Monad
casi per funzioni, in questo modo:
doBoth :: (Monad m) => (a -> m b) -> (a -> m c) -> a -> m c
doBoth f g = (>>) <$> f <*> g
...o, se si preferisce:
doBoth :: (Monad m) => (a -> m b) -> (a -> m c) -> a -> m c
doBoth = liftA2 (>>)
Naturalmente, liftA2
è equivalente a liftM2
così si potrebbe chiedere se il sollevamento un'operazione su monadi in un altro monade ha qualcosa a che fare con i trasformatori monade; in generale, il rapporto non è imbarazzante, ma in questo caso funziona facilmente, dando qualcosa di simile:
doBoth :: (Monad m) => ReaderT a m b -> ReaderT a m c -> ReaderT a m c
doBoth = (>>)
... modulo di avvolgimento appropriate e quali, naturalmente. Per tornare alla versione originale, l'uso originale di return
ora deve essere qualcosa con tipo ReaderT a m a
, che non dovrebbe essere troppo difficile da riconoscere come la funzione ask
per le monodie dei lettori.
L'altra prospettiva è riconoscere che funzioni con tipi come (Monad m) => a -> m b
possono essere composti direttamente, proprio come funzioni pure. La funzione (<=<) :: Monad m => (b -> m c) -> (a -> m b) -> (a -> m c)
fornisce un equivalente diretto alla composizione di funzione (.) :: (b -> c) -> (a -> b) -> (a -> c)
oppure è possibile invece utilizzare Control.Category
e il wrapper newtype
Kleisli
per lavorare con la stessa cosa in modo generico.
Abbiamo ancora bisogno di dividere l'argomento, tuttavia, così che cosa abbiamo veramente bisogno qui è una composizione "ramificazione", che Category
da sola non ha; utilizzando Control.Arrow
così otteniamo (&&&)
, facendoci riscrivere la funzione come segue:
doBoth :: (Monad m) => Kleisli m a b -> Kleisli m a c -> Kleisli m a (b, c)
doBoth f g = f &&& g
Dato che non ci interessa circa il risultato della prima freccia Kleisli, solo i suoi effetti collaterali, possiamo scartare che la metà di la tupla in modo ovvio:
doBoth :: (Monad m) => Kleisli m a b -> Kleisli m a c -> Kleisli m a c
doBoth f g = f &&& g >>> arr snd
Che ci riporta al modulo generico. Specializzata per l'originale, il return
ora diventa semplicemente id
:
constKleisli :: (Monad m) => Kleisli m a b -> Kleisli m a a
constKleisli f = f &&& id >>> arr snd
Poiché le funzioni regolari sono anche Arrow
s, la definizione di cui sopra funziona anche lì se si generalizza la firma tipo. Tuttavia, può essere illuminante per espandere la definizione che ne deriva per funzioni pure e semplificare come segue:
\f x -> (f &&& id >>> arr snd) x
\f x -> (snd . (\y -> (f y, id y))) x
\f x -> (\y -> snd (f y, y)) x
\f x -> (\y -> y) x
\f x -> x
.
Quindi siamo tornati allo flip const
, come previsto!
In breve, la funzione è una certa variazione su entrambi (>>)
o flip const
, ma in modo che si basa sulle differenze - ex utilizzando sia un ambiente ReaderT
e (>>)
della monade sottostante, quest'ultimo utilizza gli effetti collaterali impliciti dello specifico Arrow
e l'aspettativa che gli effetti collaterali Arrow
si verifichino in un ordine particolare. A causa di questi dettagli, non è probabile che vi siano generalizzazioni o semplificazioni disponibili. In un certo senso, la definizione che stai usando è esattamente tanto semplice quanto deve essere, motivo per cui le definizioni alternative che ho dato sono più lunghe e/o implicano una certa quantità di wrapping e unwrapping.
Una funzione come questa sarebbe un'aggiunta naturale di una "libreria di utilità monad" di qualche tipo. Mentre Control.Monad
fornisce alcuni combinatori lungo queste linee, è tutt'altro che esauriente e non potrei né trovare né richiamare alcuna variazione su questa funzione nelle librerie standard. Non sarei affatto sorpreso di trovarlo in una o più librerie di utilità su hackage, tuttavia.
Avendo in gran parte rinunciato alla questione dell'esistenza, non posso davvero offrire molte indicazioni su come nominare al di là di ciò che è possibile trarre dalla discussione precedente sui concetti correlati.
Come ultimo accenno, si noti anche che la propria funzione non ha scelte di flusso di controllo basate sul risultato di un'espressione monadica, poiché l'esecuzione delle espressioni non importa quale sia l'obiettivo principale. Avere una struttura computazionale indipendente dal contenuto parametrico (vale a dire la roba di tipo Monad m => m a
) è in genere un segno che non è effettivamente necessario un intero Monad
, e potrebbe ottenere con la nozione più generale di Applicative
.
Sembra che sia possibile definire questa funzione utilizzando semplici funzioni: 'constF :: Functor f => (a -> f b) -> a -> f a; constF f a = a <$ f a' – fuz
@FUZxxl, anche questo funziona, grazie ... è questo uso corretto del nome const allora? –
Cosa intendi? – fuz