tur/arrow
arrow combinators over the (->) function arrow, in two surfaces.
Bare functions (the simple path): arr, >>>, arrow-first, arrow-second, par-comp, arrow-split, arrow-const, and arrow-dup as plain functions -- use these when you do not need polymorphism over the arrow constructor.
Typeclass dispatch (parametric over any Arrow): the Haskell-style Arrow / ArrowZero / ArrowPlus / ArrowChoice / ArrowLoop / ArrowApply hierarchy with its (->) instances, declared lower in this file. Use these when you want code resolved through the instance dictionary. The two surfaces share the method names arr / >>>: a bare call dispatches to the matching instance when the receiver's type selects one, and falls back to the bare combinator otherwise (see docs/archive/history/typeclass-methods-share-value-namespace-with-defns.md).
Feedback (ArrowLoop): arrow-loop-lazy, arrow-loop-fix, and arrow-loop-delay close the d output of an arrow back onto its d input, each at a different time -- see the "ArrowLoop feedback cells" section below, and docs/guides/arrows-guide.md ("ArrowLoop -- feedback").
The typeclass layer was scaled back in docs/archive/history/stdlib-arrow-scaleback-plan.md and reintroduced -- then merged back into this single module -- per docs/archive/history/stdlib-arrow-typeclass-reintroduction-plan.md.
Since: Phase CA0
arr
(arr [^fat f])
lift a function to an arrow (identity for function arrows).
| f | the function to lift |
The function itself.
(arr (fn [x] (+ x 1))) ; => identity for functions
>>>
(>>> [A B C])
compose two function arrows sequentially (register-class-correct).
Polymorphic over the element type: the type parameters A, B, C are bound at each call site from the declared types of f and g, so a float pipeline emits a double-returning inner body dispatched through tur_thunk_double_double_t (register-class-correct), and an int pipeline uses the standard int64 thunk ABI.
| f | first arrow (A -> B) | |
| g | second arrow (B -> C) |
A closure (fn [A] C) that applies f then g. Typed as a function, not ptr<void>, so the result can be called directly at its element types.
((>>> (fn [x : float] : float (+ x 1.0))
(fn [x : float] : float (* x 2.0))) 3.0) ; => 8.0
Since: Phase CA1
arrow-first
(arrow-first [^fat f])
apply a function to the first component of a Tuple2.
| f | function to apply |
A function on Tuple2 that applies f to the first field.
((arrow-first (fn [x] (+ x 1))) <heap Tuple2 (5, 10)>) ; => Tuple2(6, 10)
arrow-second
(arrow-second [^fat f])
apply a function to the second component of a Tuple2.
| f | function to apply |
A function on Tuple2 that applies f to the second field.
((arrow-second (fn [x] (+ x 1))) <heap Tuple2 (5, 10)>) ; => Tuple2(5, 11)
par-comp
(par-comp [a b c d ^fat f ^fat g])
parallel composition of two functions on Tuple2 types.
| f | function a -> b | |
| g | function c -> d |
A function on Tuple2 a c -> Tuple2 b d: (par-comp f g)(tuple2 x y) = tuple2(f x, g y).
((par-comp inc double) (tuple2 1 2)) ; => Tuple2(2, 4)
arrow-split
(arrow-split [a b c ^fat f ^fat g])
duplicate input and apply two functions, returning a Tuple2.
| f | function a -> b | |
| g | function a -> c |
A function a -> Tuple2 b c: (arrow-split f g)(x) = tuple2(f x, g x).
((arrow-split inc double) 3) ; => Tuple2(4, 6)
arrow-const
(arrow-const [v])
arrow that ignores its input and always returns v.
| v | the constant value to return |
An arrow (any -> v).
((arrow-const 42) 999) ; => 42
Since: Phase B2
arrow-dup
(arrow-dup [x] :int)
arrow that duplicates its input into a Tuple2.
| x | the value to duplicate |
A Tuple2 of x with itself: Tuple2(x, x).
(tuple2-1st (arrow-dup 7)) ; => 7
Since: Phase B2
LoopCell
(defopaque LoopCell :int)
the fed-back d slot of an ArrowLoop, as a handle.
Carrier is :int (the erased two-slot pair carrier the arrow layer runs on), but the newtype keeps an arbitrary integer out of loop-cell-force: only a cell minted by one of the arrow-loop combinators is accepted.
Since: arrowloop-lazy-feedback
loop-cell-of
(loop-cell-of [slot :int] :LoopCell)
read slot 1 of an ArrowLoop input pair as a LoopCell.
The arrow layer erases every slot to the int64 carrier, so a looped arrow written in Turmeric receives the feedback cell as a plain int. This is the one sanctioned way back to the handle type.
| slot | the erased slot-1 value handed to the looped arrow |
The same handle, typed as a LoopCell.
(loop-cell-force (loop-cell-of d)) ; => the fed-back value
Since: arrowloop-lazy-feedback
loop-cell-ready?
(loop-cell-ready? [c :LoopCell] :bool)
has the fed-back value been written yet?
False exactly inside an arrow-loop run, before the looped arrow has returned its d output. arrow-loop-fix and arrow-loop-delay seed the cell up front, so it reads true there from the first call.
| c | the feedback cell |
true when loop-cell-force would succeed, false when it would black-hole.
(if (loop-cell-ready? c) (loop-cell-force c) 0)
Since: arrowloop-lazy-feedback
loop-cell-force
(loop-cell-force [c :LoopCell] :int)
read the fed-back value out of a feedback cell.
Panics with <<loop>> when the value is not there yet -- that is the black hole, and it is a genuine error in the looped arrow (it demanded its own output). Guard with loop-cell-ready? when a miss is expected.
| c | the feedback cell |
The fed-back d value, in the erased arrow carrier.
(let [lp (arrow-loop step)] (loop-cell-force (loop-cell-of (lp 7))))
Since: arrowloop-lazy-feedback
Category
(defclass Category [arr])
the base of the arrow hierarchy: an identity arrow and sequential composition.
(ident [] :arr)-
the identity arrow (nullary; the result is the arrow itself). Resolved by return-type / unique-instance dispatch (there is no argument to dispatch on); see docs/archive/history/return-type-dispatch-nullary-arrow-methods-plan.md.
(comp [f g] :arr)-
left-to-right composition: run f, then g (comp f g == g . f over (->)). Distinct method name from Arrow's >>> so the two surfaces coexist without an operator-mangling collision.
A second instance (e.g. Kleisli, stdlib/kleisli.tur) makes a bare (ident) with no expected type ambiguous -- the Mechanism-B uniqueness gate then fires the existing ambiguity error, resolvable by ascription ((:: (ident) :Kleisli)).
Since: Phase CA2
Arrow
(defclass Arrow [a] [(Category a)])
lift functions into arrows and wire them together.
(arr [f] :a)-
lift a plain function (b -> c) into an arrow a b c.
(>>> [f :(a b c) g :(a c d)] :(a b d))-
left-to-right composition: run the left arrow, then the right.
(<<< [f :(a c d) g :(a b c)] :(a b d))-
right-to-left composition (flipped >>>).
(first [f] :a)-
act on the first component of a pair, passing the second through.
(second [f] :a)-
act on the second component of a pair, passing the first through.
>>> and <<< spell their arrows -- (a b c) is the arrow from b to c, (fn [b] c) at the (->) head -- so a call with concrete functions specializes the instance at their element types, and the composed closure is called at exactly those types.
Superclass: Category, so an [^Arrow A] body may also use ident/comp.
Since: Phase CA1
ArrowZero
(defclass ArrowZero [a] [(Category a)])
an arrow with a distinguished empty/zero arrow.
(zero-arrow [] :a)-
the arrow with no behaviour (nullary; result is the arrow).
The function arrow (->) is deliberately NOT made an instance: a total function b -> c with no input requires choosing an inhabitant of c, which (->) cannot do generically. The class is declared so other arrow constructors (e.g. a future Kleisli) can instantiate it.
Superclass: Category -- not Arrow, as Haskell has it. stdlib/kleisli.tur's Kleisli is a Category with an honest zero arrow and deliberately no Arrow instance, and the zero-arrow laws (zero >>> f = zero) need only composition.
Since: Phase CA1
ArrowPlus
(defclass ArrowPlus [a] [(ArrowZero a)])
an ArrowZero whose arrows form a monoid under choice.
(plus-arrow [f g] :a)-
combine two arrows (the monoid append).
Like ArrowZero, (->) is not instantiated here; the class exists for arrow constructors that carry a sensible alternative/merge.
Superclass: ArrowZero.
Since: Phase CA1
ArrowChoice
(defclass ArrowChoice [a] [(Arrow a)])
route an arrow over one arm of a sum (Either).
(left [f] :a)-
apply the arrow to a Left payload, pass Right through unchanged.
(right [f] :a)-
apply the arrow to a Right payload, pass Left through unchanged.
(+++ [f g] :a)-
run the left arrow on Left, the right arrow on Right (fan over sum).
(||| [f g] :a)-
run either arrow and collapse both arms to a common result.
Requires the Either sum type (stdlib/either.tur).
Superclass: Arrow.
Since: Phase CA1
ArrowLoop
(defclass ArrowLoop [a] [(Arrow a)])
feed part of an arrow's output back as input.
(arrow-loop [f] :a)-
given an arrow a (b, d) (c, d), produce an arrow a b c by feeding the d output back as the d input.
Turmeric is strict, so the fed-back d is handed to the looped arrow as a LoopCell rather than as a bare value: the (->) instance runs the arrow on (b, empty-cell) and fills the cell with the d output afterwards. An arrow that ignores d behaves exactly as before; an arrow that parks the cell in its c output and forces it later gets true lazy feedback; an arrow that forces the cell mid-run hits the black hole (a <<loop>> panic) instead of silently reading a sentinel.
For a looped arrow that must read d *immediately*, use the bare arrow-loop-fix (iterate to a fixpoint) or arrow-loop-delay (a unit delay in the feedback path) below.
Superclass: Arrow.
Since: Phase CA1 (lazy feedback: arrowloop-lazy-feedback)
ArrowApply
(defclass ArrowApply [a] [(Arrow a)])
apply an arrow that is itself a value flowing through.
(app [] :a)-
the arrow a (a b c, b) c that applies its first component (an arrow) to its second (nullary; result is the arrow).
Superclass: Arrow.
Since: Phase CA1
Arrow[(->)]
(definstance Arrow [(->)])
the function arrow: arr is the identity (a plain function is already an arrow), >>> / <<< are composition, first / second act on a heap pair.
ArrowChoice[(->)]
(definstance ArrowChoice [(->)])
map an arrow over one arm of an Either, passing the other arm through; +++ fans two arrows over the two arms, ||| collapses them.
ArrowLoop[(->)]
(definstance ArrowLoop [(->)])
lazy (knot-tying) feedback: run the arrow on (b, empty-cell), fill the cell with the d output, project the c output.
arrow-loop-lazy
(arrow-loop-lazy [^fat f] :ptr<void>)
the bare-surface twin of the ArrowLoop [(->)] instance.
Feeds the d output back through a LoopCell that is filled only once the looped arrow returns. Reads of the cell that the arrow defers -- by parking the handle in its own c output -- observe the d that this same run produced; a read made during the run is the <<loop>> black hole.
| f | the looped arrow, a (b, LoopCell) -> (c, d) over the heap-pair layout |
A ptr<void> closure b -> c.
;; f parks the cell in c, so forcing c after the loop yields d = b * 10. (let [lp (arrow-loop-lazy park-cell-step)] (loop-cell-force (loop-cell-of (lp 7)))) ; => 70
Since: arrowloop-lazy-feedback
arrow-loop-fix
(arrow-loop-fix [^fat f d0 :int fuel :int] :ptr<void>)
feedback by iteration to a fixpoint.
For a looped arrow that reads d *strictly*: the cell is seeded with d0 and the arrow is re-run, each time seeing the previous run's d, until d stops changing or fuel iterations have elapsed. The result is the c output of the last iteration. Non-convergence is not an error here -- it simply spends the fuel and returns the last c -- so a diverging arrow costs a bounded amount of work rather than hanging.
| f | the looped arrow, a (b, LoopCell) -> (c, d) | |
| d0 | the seed for the fed-back component | |
| fuel | maximum iterations; anything below 1 runs exactly one pass |
A ptr<void> closure b -> c.
;; newton-step: d' = (d + b/d)/2, c = d -- integer sqrt by fixpoint. ((arrow-loop-fix newton-step 1 32) 144) ; => 12
Since: arrowloop-lazy-feedback
arrow-loop-delay
(arrow-loop-delay [^fat f d0 :int] :ptr<void>)
feedback through a unit delay (step-indexed).
The fed-back component persists across calls: call n sees the d that call n-1 produced, and the first call sees d0. This is the always-total member of the family -- there is no black hole and no iteration -- and it is the feedback shape signal-processing and state-machine arrows want, where the loop is closed over time rather than within a single step.
The returned arrow is stateful: it owns one cell, so each call advances the same feedback line. Call arrow-loop-delay again for an independent line.
| f | the looped arrow, a (b, LoopCell) -> (c, d) | |
| d0 | the value the first call sees fed back |
A ptr<void> closure b -> c, carrying its own feedback state.
;; sum-step: d' = d + b, c = d + b -- a running total. (let [acc (arrow-loop-delay sum-step 0)] (acc 1) (acc 2) (acc 3)) ; => 1, 3, 6
Since: arrowloop-lazy-feedback
ArrowApply[(->)]
(definstance ArrowApply [(->)])
the apply arrow: run the arrow in slot 0 on slot 1.
Category[(->)]
(definstance Category [(->)])
the function arrow: ident is the plain identity function, comp is forward composition. The eta-expanded (fn [x] x) (rather than a bare identity reference) is required so the dispatched nullary call keeps a concrete result type -- see docs/archive/history/instance-method-returning-untyped-param-loses-result-type.md.
Internal definitions
__arrow_pair_first-- inline-C: apply fv to e1 of a heap pair *p,__arrow_pair_second-- inline-C: apply fv to e2 of a heap pair *p,__arrow_pair_par-- inline-C: apply fv to e1 and gv to e2 of *p.__arrow_pair_split-- inline-C: produce a heap pair from (fv x, gv x).__arrow_pair_dup-- inline-C: produce a heap pair with both fields = x.__ac_pair_first-- inline-C: apply fv to slot 0 of a heap pair, copy slot 1.__ac_pair_second-- inline-C: copy slot 0 of a heap pair, apply fv to slot 1.__ac_cell_new-- inline-C: allocate a feedback cell, filled or empty.__ac_loop_step-- inline-C: the lazy (knot-tying) ArrowLoop step. Allocate__ac_loop_fix_step-- inline-C: the strict fixpoint ArrowLoop step. Seed a__ac_loop_delay_step-- inline-C: one step of the unit-delay ArrowLoop. The__ac_app-- inline-C: ArrowApply for (->). The input pair carries an arrow