Wat zijn pijlen en hoe kan ik ze gebruiken?

Ik heb geprobeerd de betekenis van pijlente leren, maar ik begreep ze niet.

Ik heb de Wikibooks-zelfstudie gebruikt. Ik denk dat het probleem van Wikibook vooral is dat het geschreven lijkt te zijn voor iemand die het onderwerp al begrijpt.

Kan iemand uitleggen wat pijlen zijn en hoe ik ze kan gebruiken?


Antwoord 1, autoriteit 100%

Ik ken geen tutorial, maar ik denk dat het het gemakkelijkst is om pijlen te begrijpen als je naar enkele concrete voorbeelden kijkt. Het grootste probleem dat ik had met het leren gebruiken van pijlen, was dat geen van de tutorials of voorbeelden daadwerkelijk laten zien hoe je pijlen gebruikt, alleen hoe je ze moet samenstellen. Dus, met dat in gedachten, hier is mijn mini-tutorial. Ik zal twee verschillende pijlen onderzoeken: functies en een door de gebruiker gedefinieerd pijltype MyArr.

-- type representing a computation
data MyArr b c = MyArr (b -> (c,MyArr b c))

1) Een pijl is een berekening van invoer van een bepaald type naar uitvoer van een bepaald type. De pijltypeklasse heeft drie typeargumenten: het pijltype, het invoertype en het uitvoertype. Als we naar de instantiekop kijken voor pijlinstanties, vinden we:

instance Arrow (->) b c where
instance Arrow MyArr b c where

De pijl (ofwel (->)of MyArr) is een abstractie van een berekening.

Voor een functie b -> c, bis de invoer en cis de uitvoer.
Voor een MyArr b cis bde invoer en cis de uitvoer.

2) Om een ​​pijlberekening daadwerkelijk uit te voeren, gebruikt u een functie die specifiek is voor uw pijltype. Voor functies past u de functie eenvoudig toe op een argument. Voor andere pijlen moet er een aparte functie zijn (net als runIdentity, runState, enz. voor monaden).

-- run a function arrow
runF :: (b -> c) -> b -> c
runF = id
-- run a MyArr arrow, discarding the remaining computation
runMyArr :: MyArr b c -> b -> c
runMyArr (MyArr step) = fst . step

3) Pijlen worden vaak gebruikt om een ​​lijst met invoer te verwerken. Voor functies kunnen deze parallel worden uitgevoerd, maar voor sommige pijlen is de output bij een bepaalde stap afhankelijk van eerdere inputs (bijv. het bijhouden van een lopend totaal van inputs).

-- run a function arrow over multiple inputs
runFList :: (b -> c) -> [b] -> [c]
runFList f = map f
-- run a MyArr over multiple inputs.
-- Each step of the computation gives the next step to use
runMyArrList :: MyArr b c -> [b] -> [c]
runMyArrList _ [] = []
runMyArrList (MyArr step) (b:bs) = let (this, step') = step b
                                   in this : runMyArrList step' bs

Dit is een van de redenen waarom pijlen nuttig zijn. Ze bieden een rekenmodel dat impliciet gebruik kan maken van de staat zonder die staat ooit aan de programmeur bloot te stellen. De programmeur kan gepijlde berekeningen gebruiken en deze combineren om geavanceerde systemen te creëren.

Hier is een MyArr die het aantal ontvangen inputs bijhoudt:

-- count the number of inputs received:
count :: MyArr b Int
count = count' 0
  where
    count' n = MyArr (\_ -> (n+1, count' (n+1)))

Nu neemt de functie runMyArrList counteen lijstlengte n als invoer en retourneert een lijst met Ints van 1 tot n.

Merk op dat we nog steeds geen “pijl”-functies hebben gebruikt, dat wil zeggen de methoden van de Arrow-klasse of functies die in termen daarvan zijn geschreven.

4) De meeste van de bovenstaande code is specifiek voor elke Arrow-instantie [1]. Alles in Control.Arrow(en Control.Category) gaat over het samenstellen van pijlen om nieuwe pijlen te maken. Als we doen alsof Categorie deel uitmaakt van Arrow in plaats van een aparte klasse:

-- combine two arrows in sequence
>>> :: Arrow a => a b c -> a c d -> a b d
-- the function arrow instance
-- >>> :: (b -> c) -> (c -> d) -> (b -> d)
-- this is just flip (.)
-- MyArr instance
-- >>> :: MyArr b c -> MyArr c d -> MyArr b d

De functie >>>heeft twee pijlen nodig en gebruikt de uitvoer van de eerste als invoer voor de tweede.

Hier is nog een andere operator, gewoonlijk “fanout” genoemd:

-- &&& applies two arrows to a single input in parallel
&&& :: Arrow a => a b c -> a b c' -> a b (c,c')
-- function instance type
-- &&& :: (b -> c) -> (b -> c') -> (b -> (c,c'))
-- MyArr instance type
-- &&& :: MyArr b c -> MyArr b c' -> MyArr b (c,c')
-- first and second omitted for brevity, see the accepted answer from KennyTM's link
-- for further details.

Omdat Control.Arroween manier biedt om berekeningen te combineren, volgt hier een voorbeeld:

-- function that, given an input n, returns "n+1" and "n*2"
calc1 :: Int -> (Int,Int)
calc1 = (+1) &&& (*2)

Ik heb vaak functies zoals calc1nuttig gevonden bij gecompliceerde vouwen, of functies die bijvoorbeeld op aanwijzers werken.

De klasse van het type Monadbiedt ons een middel om monadische berekeningen te combineren in een enkele nieuwe monadische berekening met behulp van de functie >>=. Op dezelfde manier biedt de klasse Arrowons de middelen om berekeningen met pijlen te combineren in een enkele nieuwe berekening met pijlen met behulp van een paar primitieve functies (first, arr, en ***, met >>>en idvan Control.Category). Ook vergelijkbaar met Monads, de vraag “Wat doet een pijl?” kan niet algemeen worden beantwoord. Het hangt af van de pijl.

Helaas ken ik niet veel voorbeelden van pijlinstanties in het wild. Functies en FRP lijken de meest voorkomende toepassingen te zijn. HXT is het enige andere belangrijke gebruik dat in je opkomt.

[1] Behalve count. Het is mogelijk om een ​​telfunctie te schrijven die hetzelfde doet voor elke instantie van ArrowLoop.


Antwoord 2, autoriteit 48%

Uit een blik op uw geschiedenis op Stack Overflow, ga ik ervan uit dat u vertrouwd bent met enkele van de andere standaardtypeklassen, met name Functoren Monoid, en begin met een korte analogie daarvan.

De enkele bewerking op Functoris fmap, die dient als een algemene versie van mapop lijsten. Dit is vrijwel het hele doel van de typeklasse; het definieert “dingen die u in kaart kunt brengen”. Dus in zekere zin vertegenwoordigt Functoreen veralgemening van dat specifieke aspect van lijsten.

De bewerkingen voor Monoidzijn gegeneraliseerde versies van de lege lijst en (++), en het definieert “dingen die associatief kunnen worden gecombineerd, met een bepaald ding dat een identiteitswaarde”. Lijsten zijn vrijwel het eenvoudigste dat aan die beschrijving voldoet, en Monoidvertegenwoordigt een veralgemening van dat aspect van lijsten.

Op dezelfde manier als de twee bovenstaande, zijn de bewerkingen van de klasse Categorygegeneraliseerde versies van iden (.), en het definieert “dingen die twee typen in een bepaalde richting met elkaar verbinden, die kop aan staart kunnen worden verbonden”. Dit vertegenwoordigt dus een veralgemening van dat aspect van functies. Met name niet opgenomen in de generalisatie zijn currying of functietoepassing.

De klasse van het type Arrowbouwt voort op Category, maar het onderliggende concept is hetzelfde: Arrowen zijn dingen die als functies en hebben een “identiteitspijl” gedefinieerd voor elk type. De aanvullende bewerkingen die zijn gedefinieerd in de klasse Arrowzelf definiëren alleen een manier om een ​​willekeurige functie naar een Arrowte tillen en een manier om twee pijlen “parallel” te combineren als een enkele pijl tussen tuples.

Dus het eerste dat u hier moet onthouden, is dat expressies die Arrows bouwen, in wezen uitgebreide functiecomposities zijn. De combinators zoals (***)en (>>>)zijn voor het schrijven van een “puntvrije” stijl, terwijl de notatie procgeeft een manier om tijdelijke namen toe te wijzen aan in- en uitgangen tijdens het bedraden.

Een handig ding om op te merken is dat, hoewel Arrows soms worden beschreven als “de volgende stap” van Monads, er echt geen erg zinvolle relatie daar. Voor elke Monadkun je werken met Kleisli-pijlen, dit zijn gewoon functies met een type als a -> m b. De operator (<=<)in Control.Monadis hiervoor een pijlsamenstelling. Aan de andere kant krijg je met Arrows geen Monadtenzij je ook de klasse ArrowApplyopneemt. Er is dus geen directe verbinding als zodanig.

Het belangrijkste verschil hier is dat terwijl Monads kunnen worden gebruikt om berekeningen te sequencen en dingen stap voor stap te doen, Arrows in zekere zin “tijdloos” zijn net als reguliere functies. Ze kunnen extra machines en functionaliteit bevatten die worden gesplitst door (.), maar het lijkt meer op het bouwen van een pijplijn, niet op het accumuleren van acties.

De andere gerelateerde typeklassen voegen extra functionaliteit toe aan een pijl, zoals het kunnen combineren van pijlen met Eitheren (,).


Mijn favoriete voorbeeld van een Arrowzijn stateful stream-transducers, die er ongeveer zo uitzien:

data StreamTrans a b = StreamTrans (a -> (b, StreamTrans a b))

Een StreamTrans-pijl converteert een invoerwaarde naar een uitvoer en een “bijgewerkte” versie van zichzelf; overweeg de manieren waarop dit verschilt van een stateful Monad.

Het schrijven van instanties voor Arrowen de bijbehorende typeklassen voor het bovenstaande type kan een goede oefening zijn om te begrijpen hoe ze werken!

Ik schreef eerder ook een vergelijkbaar antwoorddie je misschien nuttig vindt.


Antwoord 3, autoriteit 41%

Ik wil graag toevoegen dat pijlen in Haskell veel eenvoudiger zijn dan ze lijken
gebaseerd op de literatuur. Het zijn gewoon abstracties van functies.

Om te zien hoe dit praktisch nuttig is, moet je bedenken dat je een heleboel
functies die u wilt samenstellen, waarvan sommige puur zijn en andere
monadisch. Bijvoorbeeld, f :: a -> b, g :: b -> m1 c, en h :: c -> m2 d.

Als ik elk van de betrokken typen ken, zou ik een compositie met de hand kunnen bouwen, maar
het uitvoertype van de compositie zou het tussenproduct moeten weerspiegelen
monadetypen (in het bovenstaande geval m1 (m2 d)). Wat als ik gewoon wilde trakteren?
de functies alsof ze gewoon a -> b, b -> c, en c -> d? Dat is,
Ik wil de aanwezigheid van monaden abstraheren en alleen redeneren over de
onderliggende typen. Ik kan pijlen gebruiken om precies dit te doen.

Hier is een pijl die de aanwezigheid van IO voor functies in de
IO monade, zodat ik ze kan samenstellen met pure functies zonder de
code samenstellen die moet weten dat IO erbij betrokken is
. We beginnen met het definiëren van een
IOArrow om IO-functies in te pakken:

data IOArrow a b = IOArrow { runIOArrow :: a -> IO b }

instance Category IOArrow where
  id = IOArrow return
  IOArrow f . IOArrow g = IOArrow $ f <=< g

instance Arrow IOArrow where
  arr f = IOArrow $ return . f
  first (IOArrow f) = IOArrow $ \(a, c) -> do
    x <- f a
    return (x, c)

Vervolgens maak ik enkele eenvoudige functies die ik wil samenstellen:

foo :: Int -> String
foo = show
bar :: String -> IO Int
bar = return . read

En gebruik ze:

main :: IO ()
main = do
  let f = arr (++ "!") . arr foo . IOArrow bar . arr id
  result <- runIOArrow f "123"
  putStrLn result

Hier roep ik IOArrow en runIOArrow, maar als ik deze pijlen zou passeren
rond in een bibliotheek met polymorfe functies, hoeven ze alleen maar te accepteren
argumenten van het type “Pijl a => a b c”. Geen van de bibliotheekcodes zou moeten
bewust worden gemaakt dat er een monade bij betrokken was. Alleen de maker en eindgebruiker van de
pijl moet weten.

Het generaliseren van IOArrow om voor functies in elke monade te werken, wordt de “Kleisli .” genoemd
pijl”, en er is al een ingebouwde pijl om precies dat te doen:

main :: IO ()
main = do
  let g = arr (++ "!") . arr foo . Kleisli bar . arr id
  result <- runKleisli g "123"
  putStrLn result

Je kunt natuurlijk ook pijlcompositieoperatoren en proc-syntaxis gebruiken om
maak het iets duidelijker dat het om pijlen gaat:

arrowUser :: Arrow a => a String String -> a String String
arrowUser f = proc x -> do
  y <- f -< x
  returnA -< y
main :: IO ()
main = do
  let h =     arr (++ "!")
          <<< arr foo
          <<< Kleisli bar
          <<< arr id
  result <- runKleisli (arrowUser h) "123"
  putStrLn result

Hier moet duidelijk zijn dat hoewel mainweet dat de IO-monade erbij betrokken is,
arrowUserniet. Er zou geen manier zijn om IO te “verbergen” voor arrowUser
zonder pijlen — niet zonder toevlucht te nemen tot unsafePerformIOom de
intermediaire monadische waarde terug in een zuivere (en dus die context verliezend)
voor altijd). Bijvoorbeeld:

arrowUser' :: (String -> String) -> String -> String
arrowUser' f x = f x
main' :: IO ()
main' = do
  let h      = (++ "!") . foo . unsafePerformIO . bar . id
      result = arrowUser' h "123"
  putStrLn result

Probeer dat te schrijven zonder unsafePerformIO, en zonder dat arrowUser'dat hoeft te doen
omgaan met argumenten van het Monad-type.


Antwoord 4, autoriteit 2%

Er zijn de aantekeningen van John Hughes van een AFP-workshop (Advanced Functional Programming). Merk op dat ze zijn geschreven voordat de Arrow-klassen werden gewijzigd in de Base-bibliotheken:

http://www.cse.chalmers.se/~rjmh/ afp-arrows.pdf


Antwoord 5

Toen ik Arrow-composities (voornamelijk Monads) begon te verkennen, was mijn benadering om de functionele syntaxis en compositie waarmee het meestal wordt geassocieerd te doorbreken en te beginnen met het begrijpen van de principes ervan met behulp van een meer declaratieve benadering. Met dit in gedachten vind ik de volgende indeling intuïtiever:

function(x) {
  func1result = func1(x)
  if(func1result == null) {
    return null
  } else {
    func2result = func2(func1result)
    if(func2result == null) {
      return null
    } else {
      func3(func2result)
    } 

Dus, in wezen, voor een bepaalde waarde x, roep eerst een functie aan waarvan we aannemen dat deze null(func1) kan retourneren, een andere die nullof worden toegewezen aan nulluitwisselbaar, tot slot, een derde functie die ook nullkan retourneren. Nu gegeven de waarde x, geef x door aan func3, alleen dan, als het nullniet retourneert, geef deze waarde door aan func2, en alleen als deze waarde niet null is, geef deze waarde door aan func1. Het is meer deterministisch en de controlestroom stelt u in staat om meer geavanceerde uitzonderingsbehandeling te bouwen.

Hier kunnen we de pijlsamenstelling gebruiken: (func3 <=< func2 <=< func1) x.

Other episodes