Skip to content

Commit 0c25a16

Browse files
Add initial documentation and instances (#1)
1 parent 049aba1 commit 0c25a16

File tree

4 files changed

+173
-29
lines changed

4 files changed

+173
-29
lines changed

README.md

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
[![Maintainer: garyb](https://img.shields.io/badge/maintainer-garyb-teal.svg)](https://github.com/garyb)
77
[![Maintainer: thomashoneyman](https://img.shields.io/badge/maintainer-thomashoneyman-teal.svg)](https://github.com/thomashoneyman)
88

9-
The library summary hasn't been written yet (contributions are welcome!). The library summary describes the library's purpose in one to three sentences.
9+
Utilities for creating and managing push-based subscriptions, inspired by the [event](https://github.com/paf31/purescript-event) library. This library is used to implement subscriptions in [Halogen](https://github.com/purescript-halogen/purescript-halogen), but it can be used independently of Halogen.
1010

1111
## Installation
1212

@@ -18,27 +18,38 @@ spago install halogen-subscriptions
1818

1919
## Quick start
2020

21-
The quick start hasn't been written yet (contributions are welcome!). The quick start covers a common, minimal use case for the library, whereas longer examples and tutorials are kept in the [docs directory](./docs).
21+
The `halogen-subscriptions` library helps you create and transform push-based subscriptions. Most subscriptions follow this pattern:
2222

23-
## Documentation
23+
1. Use the `create` function to produce a paired `Emitter` and `Listener`. An emitter is a possibly-infinite list of values that you can subscribe to, and a listener is a mechanism for pushing values to the emitter.
24+
2. Use the `subscribe` function to subscribe to outputs from the emitter by providing a callback function to run each time a value is emitted.
25+
3. Use the `notify` function to push values to the emitter via the listener.
26+
4. Use the `unsubscribe` function to end a subscription to an emitter.
27+
28+
Here's a simple example that logs "Hello" and then "Goodbye":
2429

25-
`halogen-subscriptions` documentation is stored in a few places:
30+
```purs
31+
module Main where
2632
27-
1. Module documentation is [published on Pursuit](https://pursuit.purescript.org/packages/purescript-halogen-subscriptions).
28-
2. Written documentation is kept in the [docs directory](./docs).
29-
3. Usage examples can be found in [the test suite](./test).
33+
import Prelude
3034
31-
If you get stuck, there are several ways to get help:
35+
import Effect (Effect)
36+
import Effect.Console as Console
37+
import Halogen.Subscription as HS
3238
33-
- [Open an issue](https://github.com/purescript-halogen/purescript-halogen-subscriptions/issues) if you have encountered a bug or problem.
34-
- [Search or start a thread on the PureScript Discourse](https://discourse.purescript.org) if you have general questions. You can also ask questions in the `#purescript` and `#purescript-beginners` channels on the [Functional Programming Slack](https://functionalprogramming.slack.com) ([invite link](https://fpchat-invite.herokuapp.com/)).
39+
main :: Effect Unit
40+
main = do
41+
{ emitter, listener } <- HS.create
3542
36-
## Contributing
43+
subscription <- HS.subscribe emitter \str -> Console.log str
3744
38-
You can contribute to `halogen-subscriptions` in several ways:
45+
HS.notify listener "Hello"
46+
HS.notify listener "Goodbye!"
3947
40-
1. If you encounter a problem or have a question, please [open an issue](https://github.com/purescript-halogen/purescript-halogen-subscriptions/issues). We'll do our best to work with you to resolve or answer it.
48+
HS.unsubscribe subscription
49+
```
4150

42-
2. If you would like to contribute code, tests, or documentation, please [read the contributor guide](./CONTRIBUTING.md). It's a short, helpful introduction to contributing to this library, including development instructions.
51+
Emitters can be combined and transformed to make more sophisticated subscriptions.
52+
53+
## Documentation
4354

44-
3. If you have written a library, tutorial, guide, or other resource based on this package, please share it on the [PureScript Discourse](https://discourse.purescript.org)! Writing libraries and learning resources are a great way to help this library succeed.
55+
Module documentation is [published on Pursuit](https://pursuit.purescript.org/packages/purescript-halogen-subscriptions).

docs/README.md

Lines changed: 0 additions & 3 deletions
This file was deleted.

src/Halogen/Subscription.purs

Lines changed: 128 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,49 @@ module Halogen.Subscription
88
, Subscription
99
, subscribe
1010
, unsubscribe
11+
, fold
12+
, filter
13+
, fix
1114
) where
1215

1316
import Prelude
1417

18+
import Control.Alt (class Alt)
19+
import Control.Alternative (class Alternative)
20+
import Control.Apply (lift2)
21+
import Control.Plus (class Plus)
1522
import Data.Array (deleteBy)
1623
import Data.Foldable (traverse_)
1724
import Data.Functor.Contravariant (class Contravariant)
25+
import Data.Maybe (Maybe(..))
1826
import Effect (Effect)
1927
import Effect.Ref as Ref
28+
import Effect.Unsafe (unsafePerformEffect)
2029
import Safe.Coerce (coerce)
2130
import Unsafe.Reference (unsafeRefEq)
2231

32+
-- | A paired `Listener` and `Emitter` produced with the `create` function.
2333
type SubscribeIO a =
2434
{ listener :: Listener a
2535
, emitter :: Emitter a
2636
}
2737

38+
-- | Create a paired `Listener` and `Emitter`, where you can push values to
39+
-- | the listener and subscribe to values from the emitter.
40+
-- |
41+
-- | ```purs
42+
-- | { emitter, listener } <- create
43+
-- |
44+
-- | -- Push values into the listener:
45+
-- | notify listener "hello"
46+
-- |
47+
-- | -- Subscribe to outputs from the emitter with a callback:
48+
-- | subscription <- subscribe emitter \value ->
49+
-- | Console.log value
50+
-- |
51+
-- | -- Unsubscribe at any time:
52+
-- | unsubscribe subscription
53+
-- | ```
2854
create :: forall a. Effect (SubscribeIO a)
2955
create = do
3056
subscribers <- Ref.new []
@@ -37,36 +63,130 @@ create = do
3763
Ref.read subscribers >>= traverse_ \k -> k a
3864
}
3965

40-
newtype Listener a = Listener (a -> Effect Unit)
41-
42-
instance contravariantListener :: Contravariant Listener where
43-
cmap f (Listener g) = Listener (g <<< f)
44-
45-
notify :: forall a. Listener a -> a -> Effect Unit
46-
notify (Listener f) a = f a
47-
66+
-- | An `Emitter` represents a collection of discrete occurrences of an event;
67+
-- | conceptually, an emitter is a possibly-infinite list of values.
68+
-- |
69+
-- | Emitters are created from real events like timers or mouse clicks and can
70+
-- | be combined or transformed with the functions and instances in this module.
71+
-- |
72+
-- | Emitters are consumed by providing a callback via the `subscribe` function.
4873
newtype Emitter a = Emitter ((a -> Effect Unit) -> Effect Subscription)
4974

5075
instance functorEmitter :: Functor Emitter where
5176
map f (Emitter e) = Emitter \k -> e (k <<< f)
5277

78+
instance applyEmitter :: Apply Emitter where
79+
apply (Emitter e1) (Emitter e2) = Emitter \k -> do
80+
latestA <- Ref.new Nothing
81+
latestB <- Ref.new Nothing
82+
Subscription c1 <- e1 \a -> do
83+
Ref.write (Just a) latestA
84+
Ref.read latestB >>= traverse_ (k <<< a)
85+
Subscription c2 <- e2 \b -> do
86+
Ref.write (Just b) latestB
87+
Ref.read latestA >>= traverse_ (k <<< (_ $ b))
88+
pure (Subscription (c1 *> c2))
89+
90+
instance applicativeEmitter :: Applicative Emitter where
91+
pure a = Emitter \k -> do
92+
k a
93+
pure (Subscription (pure unit))
94+
95+
instance altEmitter :: Alt Emitter where
96+
alt (Emitter f) (Emitter g) = Emitter \k -> do
97+
Subscription c1 <- f k
98+
Subscription c2 <- g k
99+
pure (Subscription (c1 *> c2))
100+
101+
instance plusEmitter :: Plus Emitter where
102+
empty = Emitter \_ -> pure (Subscription (pure unit))
103+
104+
instance alternativeEmitter :: Alternative Emitter
105+
106+
instance semigroupEmitter :: Semigroup a => Semigroup (Emitter a) where
107+
append = lift2 append
108+
109+
instance monoidEmitter :: Monoid a => Monoid (Emitter a) where
110+
mempty = Emitter mempty
111+
112+
-- | Make an `Emitter` from a function which accepts a callback and returns an
113+
-- | unsubscription function.
114+
-- |
115+
-- | Note: You should use `create` unless you need explicit control over
116+
-- | unsubscription.
53117
makeEmitter
54118
:: forall a
55119
. ((a -> Effect Unit) -> Effect (Effect Unit))
56120
-> Emitter a
57121
makeEmitter = coerce
58122

123+
-- | Conceptually, a `Listener` represents an input source to an `Emitter`. You
124+
-- | can push a value to its paired emitter with the `notify` function.
125+
newtype Listener a = Listener (a -> Effect Unit)
126+
127+
instance contravariantListener :: Contravariant Listener where
128+
cmap f (Listener g) = coerce (g <<< f)
129+
130+
-- | Push a value to the `Emitter` paired with the provided `Listener` argument.
131+
-- |
132+
-- | ```purs
133+
-- | -- Create an emitter and listener with `create`:
134+
-- | { emitter, listener } <- create
135+
-- |
136+
-- | -- Then, push values to the emitter via the listener with `notify`:
137+
-- | notify listener "hello"
138+
-- | ```
139+
notify :: forall a. Listener a -> a -> Effect Unit
140+
notify (Listener f) a = f a
141+
142+
-- | A `Subscription` results from subscribing to an `Emitter` with `subscribe`;
143+
-- | the subscription can be ended at any time with `unsubscribe`.
59144
newtype Subscription = Subscription (Effect Unit)
60145

61146
derive newtype instance semigroupSubscription :: Semigroup Subscription
62147
derive newtype instance monoidSubscription :: Monoid Subscription
63148

149+
-- | Subscribe to an `Emitter` by providing a callback to run on values produced
150+
-- | by the emitter:
151+
-- |
152+
-- | ```purs
153+
-- | -- Produce an emitter / listener pair with `create`:
154+
-- | { emitter, listener } <- create
155+
-- |
156+
-- | -- Then, subscribe to the emitter by providing a callback:
157+
-- | subscription <- subscribe emitter \emitted ->
158+
-- | doSomethingWith emitted
159+
-- |
160+
-- | -- End the subscription at any time with `unsubscribe`:
161+
-- | unsubscribe subscription
162+
-- | ```
64163
subscribe
65164
:: forall r a
66165
. Emitter a
67166
-> (a -> Effect r)
68167
-> Effect Subscription
69168
subscribe (Emitter e) k = e (void <<< k)
70169

170+
-- | End a subscription to an `Emitter`.
71171
unsubscribe :: Subscription -> Effect Unit
72172
unsubscribe (Subscription unsub) = unsub
173+
174+
-- | Fold over values received from some `Emitter`, creating a new `Emitter`.
175+
fold :: forall a b. (a -> b -> b) -> Emitter a -> b -> Emitter b
176+
fold f (Emitter e) b = Emitter \k -> do
177+
result <- Ref.new b
178+
e \a -> Ref.modify (f a) result >>= k
179+
180+
-- | Create an `Emitter` which only fires when a predicate holds.
181+
filter :: forall a. (a -> Boolean) -> Emitter a -> Emitter a
182+
filter p (Emitter e) = Emitter \k -> e \a -> if p a then k a else pure unit
183+
184+
-- | Compute a fixed point.
185+
fix :: forall i o. (Emitter i -> { input :: Emitter i, output :: Emitter o }) -> Emitter o
186+
fix f = Emitter \k -> do
187+
Subscription c1 <- subscribe input (notify listener)
188+
Subscription c2 <- subscribe output k
189+
pure (Subscription (c1 *> c2))
190+
where
191+
{ emitter, listener } = unsafePerformEffect create
192+
{ input, output } = f emitter

test/Main.purs

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,26 @@ module Test.Main where
22

33
import Prelude
44

5+
import Data.Array (replicate)
6+
import Data.Foldable (sequence_)
57
import Effect (Effect)
6-
import Effect.Class.Console (log)
8+
import Effect.Ref as Ref
9+
import Halogen.Subscription as HS
10+
import Test.Assert (assertEqual)
711

812
main :: Effect Unit
913
main = do
10-
log "🍝"
11-
log "You should add some tests."
14+
emittedCount <- Ref.new 0
15+
{ emitter, listener } <- HS.create
16+
17+
-- The count should increment after subscribing to the emitter with a counter.
18+
sub <- HS.subscribe emitter \_ -> Ref.modify_ (_ + 1) emittedCount
19+
_ <- sequence_ $ replicate 5 (HS.notify listener "hello")
20+
count0 <- Ref.read emittedCount
21+
assertEqual { actual: count0, expected: 5 }
22+
23+
-- The count should not change after unsubscribing from the emitter.
24+
HS.unsubscribe sub
25+
_ <- sequence_ $ replicate 5 (HS.notify listener "hello")
26+
count1 <- Ref.read emittedCount
27+
assertEqual { actual: count1, expected: 5 }

0 commit comments

Comments
 (0)