qual è lo scopo delle frecce?


63

Sto imparando la programmazione funzionale con Haskell e cerco di afferrare i concetti capendo prima perché ne ho bisogno.

Mi piacerebbe conoscere l'obiettivo delle frecce nei linguaggi di programmazione funzionale. Quale problema risolvono? Ho controllato http://en.wikibooks.org/wiki/Haskell/Understanding_arrows e http://www.cse.chalmers.se/~rjmh/afp-arrows.pdf . Tutto quello che capisco è che sono usati per descrivere grafici per calcoli e che consentono una codifica più semplice senza punti.

L'articolo presume che lo stile senza punti sia generalmente più facile da capire e da scrivere. Questo mi sembra abbastanza soggettivo. In un altro articolo ( http://en.wikibooks.org/wiki/Haskell/StephensArrowTutorial#Hangman:_Main_program ), viene implementato un gioco dell'impiccato, ma non riesco a vedere come le frecce rendono naturale questa implementazione.

Potrei trovare molti documenti che descrivono il concetto, ma nulla sulla motivazione.

Cosa mi manca

Risposte:


43

Mi rendo conto che arrivo tardi alla festa, ma qui hai avuto due risposte teoriche e volevo fornire un'alternativa pratica da masticare. Sto arrivando a questo come un parente di Haskell che tuttavia è stato recentemente sfoggiato con forza sull'argomento Arrows per un progetto a cui sto attualmente lavorando.

Innanzitutto, puoi risolvere in modo produttivo la maggior parte dei problemi in Haskell senza raggiungere Frecce. Ad alcuni notevoli Haskeller non piacciono davvero e non li usano (vedi qui , qui e qui per ulteriori informazioni al riguardo). Quindi, se stai dicendo a te stesso "Ehi, non ho bisogno di questi", capisci che potresti davvero avere ragione.

Ciò che ho trovato più frustrante riguardo a Frecce quando le ho apprese per la prima volta è stato il modo in cui i tutorial sull'argomento hanno inevitabilmente raggiunto l'analogia dei circuiti. Se osservi il codice Arrow, almeno la varietà zuccherata, non assomiglia a niente come un linguaggio di definizione hardware. I tuoi ingressi si allineano a destra, i tuoi output a sinistra e se non riesci a collegarli tutti correttamente, semplicemente non si attivano. Ho pensato: Davvero? È qui che siamo finiti? Abbiamo creato un linguaggio così completamente di alto livello che ancora una volta è costituito da fili di rame e saldatura?

La risposta corretta a questo, per quanto ho potuto determinare, è: in realtà sì. Il caso d'uso killer in questo momento per Arrows è FRP (pensa a Yampa, giochi, musica e sistemi reattivi in ​​generale). Il problema con FRP è in gran parte lo stesso problema che si presenta a tutti gli altri sistemi di messaggistica sincrona: come collegare un flusso continuo di input in un flusso continuo di output senza far cadere le informazioni rilevanti o perdere perdite. Puoi modellare i flussi come elenchi - molti recenti sistemi FRP usano questo approccio - ma quando hai molti elenchi di ingressi diventa quasi impossibile da gestire. Devi isolarti dalla corrente.

Ciò che le frecce consentono nei sistemi FRP è la composizione delle funzioni in una rete e allo stesso tempo sottragga completamente qualsiasi riferimento ai valori sottostanti che vengono passati da tali funzioni. Se sei nuovo di FP, questo può inizialmente essere fonte di confusione e poi strabiliante quando ne hai assorbito le implicazioni. Di recente hai assorbito l'idea che le funzioni possono essere astratte e come capire un elenco come [(*), (+), (-)]essere di tipo [(a -> a -> a)]. Con le frecce puoi spingere ulteriormente l'astrazione di un livello.

Questa capacità aggiuntiva di astrarre porta con sé i suoi pericoli. Per prima cosa, può spingere GHC in casi angolari in cui non sa cosa fare delle ipotesi di tipo. Dovrai essere pronto a pensare a livello di tipo: questa è un'eccellente opportunità per conoscere tipi, RankNTypes e altri argomenti simili.

Ci sono anche un certo numero di esempi di ciò che definirei "Stupid Arrow Stunts" in cui il programmatore raggiunge un combinatore Arrow solo perché vuole mostrare un trucco accurato con le tuple. (Ecco il mio contributo insignificante alla follia .) Sentiti libero di ignorare un tale hot-dogging quando ti imbatti in natura.

NOTA: come ho detto sopra, sono un noob relativo. Se ho promulgato eventuali idee sbagliate sopra, non esitate a correggermi.


2
Sono felice di non aver ancora accettato nulla. Grazie per aver fornito questa risposta. È più focalizzato sugli utenti. Gli esempi sono fantastici. Le parti soggettive sono chiaramente definite ed equilibrate. Spero che le persone che hanno votato a favore di questa domanda tornino a vederlo.
Simon Bergot,

Mentre le frecce sono sicuramente lo strumento sbagliato per la tua soluzione collegata, sento che devo menzionare che removeAt' n = arr(\ xs -> (xs,xs)) >>> arr (take (n-1)) *** arr (drop n) >>> arr (uncurry (++)) >>> returnApuò essere scritto in modo più conciso e chiaro removeAt' n = (arr (take $ n-1) &&& arr (drop n)) >>> (arr $ uncurry (++)).
cemper93,

30

Questa è una specie di risposta "soft", e non sono sicuro che qualche riferimento lo indichi in questo modo, ma è così che sono arrivato a pensare alle frecce:

Un tipo di freccia A b cè fondamentalmente una funzione b -> cma con più struttura allo stesso modo in cui un valore monadico M aha più struttura di un semplice vecchio a.

Ora, quale struttura extra dipende dalla particolare istanza di freccia di cui stai parlando. Proprio come con le monadi IO ae Maybe aognuna ha una struttura aggiuntiva diversa.

La cosa che ottieni con le monadi è l'incapacità di passare da una M aa un'altra a. Ora questo può sembrare un limite, ma in realtà è una caratteristica: il sistema dei tipi ti protegge dalla trasformazione di un valore monadico in un semplice valore vecchio. Puoi utilizzare il valore solo partecipando alla monade tramite >>=o tramite le operazioni primitive della particolare istanza della monade.

Allo stesso modo, la cosa che si ottiene A b cè l'incapacità di costruire una nuova "funzione" che produce c-consumando b. La freccia ti protegge dal consumo be dalla creazione di cun'eccezione partecipando ai vari combinatori di frecce o utilizzando le operazioni primitive della particolare istanza di freccia.

Ad esempio, le funzioni del segnale in Yampa sono approssimativamente (Time -> a) -> (Time -> b), ma inoltre devono obbedire a una certa limitazione di causalità : l'uscita al momento tè determinata dai valori passati del segnale in ingresso: non si può guardare al futuro. Quindi, invece di programmare (Time -> a) -> (Time -> b), invece di programmare , SF a bcostruisci e costruisci le tue funzioni di segnale partendo da primitive. Accade così che dato che SF a bsi comporta in modo molto simile a una funzione, quindi quella struttura comune è quella che viene chiamata "freccia".


"La freccia ti protegge dal consumo be dalla creazione di cun'eccezione partecipando ai vari combinatori di frecce o utilizzando le operazioni primitive della particolare istanza di freccia." Mi scuso per aver risposto a questa antica risposta: questa frase mi ha fatto pensare a tipi lineari, cioè che le risorse non possono essere clonate o svanite. Pensi che potrebbe esserci qualche connessione?
glaebhoerl,

14

Mi piace pensare ad Arrows, come Monads e Functors, come a consentire al programmatore di fare composizioni esotiche di funzioni.

Senza Monadi o Frecce (e Functor), la composizione delle funzioni in un linguaggio funzionale si limita ad applicare una funzione al risultato di un'altra funzione. Con monadi e funzioni, è possibile definire due funzioni e quindi scrivere un codice riutilizzabile separato che specifichi in che modo tali funzioni, nel contesto di una particolare monade, interagiscono tra loro e con i dati che vi vengono trasmessi. Questo codice è inserito nel codice vincolante della Monade. Quindi una monade è una vista unica, solo un contenitore per il codice bind riutilizzabile. Le funzioni si compongono in modo diverso nel contesto di una monade da un'altra monade.

Un semplice esempio è la monade Maybe, dove esiste un codice nella funzione bind tale che se una funzione A è composta con una funzione B all'interno di una monade Maybe, e B produce un Nothing, il codice bind assicurerà che la composizione del due funzioni generano un Niente, senza preoccuparsi di applicare A al valore Nulla che esce da B. Se non ci fosse monade, il programmatore dovrebbe scrivere il codice in A per testare un input Nulla.

Le monadi significano anche che il programmatore non ha bisogno di digitare esplicitamente i parametri richiesti da ciascuna funzione nel codice sorgente - la funzione bind gestisce il passaggio dei parametri. Quindi, usando le monadi, il codice sorgente può iniziare ad apparire più come una catena statica di nomi di funzioni, piuttosto che sembrare che la funzione A "chiama" la funzione B con i parametri C e D - il codice inizia a sembrare più un circuito elettronico che un macchina in movimento - più funzionale che indispensabile.

Le frecce collegano anche le funzioni con una funzione di associazione, fornendo funzionalità riutilizzabili e nascondendo i parametri. Ma le frecce possono esse stesse essere collegate tra loro e composte e, facoltativamente, possono instradare i dati ad altre frecce in fase di esecuzione. Ora puoi applicare i dati a due percorsi di Frecce, che "fanno cose diverse" ai dati e riassemblare il risultato. Oppure puoi selezionare a quale ramo di Frecce passare i dati, a seconda di un valore nei dati. Il codice risultante è ancora più simile a un circuito elettronico, con interruttori, ritardi, integrazione ecc. Il programma sembra molto statico e non dovresti essere in grado di vedere molta manipolazione dei dati in corso. Ci sono sempre meno parametri a cui pensare e meno bisogno di pensare a quali valori i parametri possono o meno accettare.

La scrittura di un programma Arrowized implica principalmente la selezione di frecce come divisori, interruttori, ritardi e integratori, il sollevamento di funzioni in quelle frecce e il collegamento delle frecce per formare frecce più grandi. Nella Arrowized Functional Reactive Programming, le frecce formano un loop, con l'input dal mondo combinato con l'output dell'ultima iterazione del programma, in modo che l'output reagisca all'input del mondo reale.

Uno dei valori del mondo reale è il tempo. In Yampa, la freccia della funzione del segnale inserisce invisibilmente il parametro time nel programma per computer: non si accede mai al valore del tempo, ma se si collega una freccia dell'integratore al programma, genererà valori integrati nel tempo che è possibile utilizzare per passare a altre frecce.


ma questo suona come un funzione applicativa (alcuni wrapper attorno a una funzione, che fornisce funzioni di supporto, in un contesto specifico, per riutilizzare funzioni già esistenti per i tipi di wrapping). ho sicuramente bisogno di leggere di più per capire, ma forse puoi aiutare, sottolineando quello che mi sto perdendo
Belun,

3

Solo un'aggiunta alle altre risposte: Personalmente mi aiuta molto a capire cos'è un tale concetto (matematicamente) e come si collega ad altri concetti che conosco.

Nel caso delle frecce, ho trovato utile il seguente documento: confronta monadi, funzioni applicative (idiomi) e frecce: gli idiomi sono ignari, le frecce sono meticolose, le monadi sono promiscue di Sam Lindley, Philip Wadler e Jeremy Yallop.

Inoltre credo che nessuno abbia menzionato questo link che potrebbe fornirti alcune idee e pubblicazioni sull'argomento.

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.