├── .gitignore ├── LICENSE ├── README.md ├── elm-package.json ├── src └── Signal │ ├── Discrete.elm │ ├── Extra.elm │ ├── Fun.elm │ ├── Stream.elm │ └── Time.elm └── test ├── Native └── ApanatshkaSignalExtra.js ├── Signal └── ExtraTest.elm ├── Test.elm └── elm-package.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore build or dist files 2 | /elm-stuff 3 | /elm.js 4 | .*.swp 5 | /test/elm-stuff 6 | /test/elm.js 7 | /test/elm.html 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Jeff Smits 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | `signal-extra` 2 | ============== 3 | 4 | Signal-related, advanced and convenience functions. 5 | 6 | `Signal.Time` 7 | ------------- 8 | Sometimes things just move too quickly. You just need to get a grip on 9 | time. This module helps you with that. Well, it helps you with 10 | time-based operations for Elm `Signal`s anyway. 11 | 12 | `Signal.Extra` 13 | -------------- 14 | Utility functions. `(~>)`, `zip`/`unzip`s, special `foldp`s, running 15 | buffers and more. 16 | 17 | `Signal.Discrete` 18 | ----------------- 19 | Reacting on events, to be used in `Signal.sampleOn` or 20 | `Signal.Discrete.folde`. 21 | 22 | `Signal.Stream` 23 | ---------------- 24 | Uninitialised signals. Forward-compatible with plans made to add this to 25 | core, and basically based on API and docs by Evan. 26 | 27 | `Signal.Fun` 28 | ------------ 29 | Utility functions for signals of functions. 30 | 31 | `Signal.Alt` 32 | ------------ 33 | Alternative signal API based on different primitives. Planned, but I 34 | make no promises. 35 | -------------------------------------------------------------------------------- /elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "5.7.0", 3 | "summary": "Signal-related, advanced and convenience functions", 4 | "repository": "https://github.com/Apanatshka/elm-signal-extra.git", 5 | "license": "MIT", 6 | "source-directories": [ 7 | "src" 8 | ], 9 | "exposed-modules": [ 10 | "Signal.Time", 11 | "Signal.Extra", 12 | "Signal.Discrete", 13 | "Signal.Stream", 14 | "Signal.Fun" 15 | ], 16 | "dependencies": { 17 | "elm-lang/core": "3.0.0 <= v < 4.0.0" 18 | }, 19 | "elm-version": "0.16.0 <= v < 0.17.0" 20 | } 21 | -------------------------------------------------------------------------------- /src/Signal/Discrete.elm: -------------------------------------------------------------------------------- 1 | module Signal.Discrete(EventSource, es, whenEqual, whenChange, whenChangeTo, folde) where 2 | {-| Helper functions for recognising events. Mostly useful in 3 | combination with `Signal.sampleOn`, although there are uses. 4 | 5 | # EventSource 6 | An `EventSource` is really just a `Signal` where we don't care about its 7 | value, but only when it updates. A prime example is `Mouse.clicks`. 8 | @docs EventSource, es 9 | 10 | # Basics 11 | @docs whenEqual, whenChange, whenChangeTo 12 | 13 | # `foldp` variations 14 | @docs folde 15 | -} 16 | 17 | import Signal exposing (Signal) 18 | 19 | {-| At some point in the future Elm will probably support something like 20 | this: 21 | 22 | type alias EventSource = Signal _ 23 | 24 | That is, `EventSource` will become an alias for any `Signal a` where we 25 | hide the `a` part. 26 | Until then, there is the `es` function to create an EventSource 27 | -} 28 | type alias EventSource = 29 | Signal () 30 | 31 | {-| Simple way to make an event signal from any signal 32 | -} 33 | es : Signal a -> EventSource 34 | es = Signal.map (always ()) 35 | 36 | {-| Fires when the value of the input signal is equal to the given 37 | value. 38 | 39 | **NB:** Repeated updates to the same value will make the `EventSource` 40 | fire repeatedly. 41 | See also: [`whenChangeTo`](#whenChangeTo). 42 | 43 | Mouse.clicks == whenEqual True Mouse.isDown 44 | -} 45 | whenEqual : a -> Signal a -> EventSource 46 | whenEqual value input = 47 | let 48 | keepIf = Signal.filter 49 | matchEvent = 50 | keepIf ((==) value) value input 51 | in 52 | es matchEvent 53 | 54 | {-| Fires when the value of the input signal changes. 55 | -} 56 | whenChange : Signal a -> EventSource 57 | whenChange input = 58 | es <| Signal.dropRepeats input 59 | 60 | {-| Fires when the value of the input signal changes to the given value. 61 | 62 | spacebarPress = whenChangeTo True Keyboard.spacebar 63 | -} 64 | whenChangeTo: a -> Signal a -> EventSource 65 | whenChangeTo value input = 66 | whenEqual value <| Signal.dropRepeats input 67 | 68 | {-| `foldp` on an `EventSource`. 69 | 70 | toggleOnEnter = folde not False <| whenChangeTo True Keyboard.enter 71 | -} 72 | folde : (b -> b) -> b -> EventSource -> Signal b 73 | folde step base evt = 74 | Signal.foldp (\_ b -> step b) base evt 75 | -------------------------------------------------------------------------------- /src/Signal/Extra.elm: -------------------------------------------------------------------------------- 1 | module Signal.Extra 2 | ( (~>), andMap, (<~), (~) 3 | , zip, zip3, zip4 4 | , unzip, unzip3, unzip4 5 | , foldp', foldps, foldps' 6 | , runBuffer, runBuffer', deltas, delayRound 7 | , keepIf, keepWhen, sampleWhen, switchWhen 8 | , keepWhenI, switchSample, keepThen 9 | , filter, filterFold 10 | , fairMerge, mergeMany, combine, mapMany, applyMany 11 | , passiveMap2, withPassive 12 | ) where 13 | {-| Utility functions that aren't in the `Signal` module from 14 | `elm-lang/core`. 15 | 16 | # Mapping 17 | @docs (<~), (~), (~>), andMap 18 | 19 | # Zipping and unzipping 20 | For those too lazy to write a record or union type. 21 | @docs zip, zip3, zip4, unzip, unzip3, unzip4 22 | 23 | # Stateful 24 | @docs foldp', foldps, foldps', runBuffer, runBuffer', deltas, delayRound 25 | 26 | # Switching 27 | @docs switchWhen,switchSample 28 | 29 | # Filters 30 | @docs keepIf,keepWhen,sampleWhen,keepThen,keepWhenI,filter,filterFold 31 | 32 | # Combining 33 | @docs fairMerge, mergeMany, combine, mapMany, applyMany, passiveMap2, withPassive 34 | -} 35 | 36 | import Signal exposing (map,map2,map3,map4,sampleOn,constant,foldp,merge,dropRepeats,filterMap) 37 | import Debug 38 | 39 | 40 | {-| An alias for `Signal.map`. A prettier way to apply a function to the current value 41 | of a signal. 42 | 43 | main : Signal Html 44 | main = 45 | view <~ model 46 | 47 | model : Signal Model 48 | 49 | view : Model -> Html 50 | -} 51 | (<~) : (a -> b) -> Signal a -> Signal b 52 | (<~) = 53 | map 54 | 55 | infixl 4 <~ 56 | 57 | 58 | {-| The `(<~)` operator, but flipped. Doesn't play well with the other 59 | two! 60 | 61 | Mouse.x ~> toFloat >> sqrt >> round 62 | >> isEven >> not 63 | >> asText 64 | -} 65 | (~>) : Signal a -> (a -> b) -> Signal b 66 | (~>) = 67 | flip map 68 | 69 | infixl 4 ~> 70 | 71 | 72 | {-| Apply a signal of functions to another signal. Like `Task.andMap`, this 73 | provides a way to combine several signals together into a data type that's 74 | easier to extend than `map2`, `map3`, etc. 75 | 76 | type alias User = 77 | { name : String 78 | , age : Int 79 | , numberOfPosts : Int 80 | } 81 | 82 | userSignal : Signal User 83 | userSignal = User 84 | `Signal.map` nameSignal 85 | `andMap` ageSignal 86 | `andMap` numberOfPostsSignal 87 | -} 88 | andMap : Signal (a -> b) -> Signal a -> Signal b 89 | andMap = 90 | map2 (<|) 91 | 92 | 93 | {-| An alias for `andMap`. Intended to be paired with the `(<~)` operator, this 94 | makes it possible for many signals to flow into a function. Think of it as a 95 | fancy alias for `mapN`. For example, the following declarations are equivalent: 96 | 97 | main : Signal Element 98 | main = 99 | scene <~ Window.dimensions ~ Mouse.position 100 | 101 | main : Signal Element 102 | main = 103 | map2 scene Window.dimensions Mouse.position 104 | 105 | You can use this pattern for as many signals as you want by using `(~)` a bunch 106 | of times, so you can go higher than `map5` if you need to. 107 | -} 108 | (~) : Signal (a -> b) -> Signal a -> Signal b 109 | (~) = 110 | andMap 111 | 112 | infixl 4 ~ 113 | 114 | 115 | {-| Zip two signals into a signal of pairs. 116 | 117 | zip Mouse.x Mouse.y == Mouse.position 118 | -} 119 | zip : Signal a -> Signal b -> Signal (a,b) 120 | zip = 121 | map2 (,) 122 | 123 | 124 | {-| -} 125 | zip3 : Signal a -> Signal b -> Signal c -> Signal (a,b,c) 126 | zip3 = 127 | map3 (,,) 128 | 129 | 130 | {-| -} 131 | zip4 : Signal a -> Signal b -> Signal c -> Signal d -> Signal (a,b,c,d) 132 | zip4 = 133 | map4 (,,,) 134 | 135 | 136 | {-| Unzip a signal of pairs to a pair of signals. 137 | 138 | unzip Mouse.position == (Mouse.x, Mouse.y) 139 | -} 140 | unzip : Signal (a,b) -> (Signal a, Signal b) 141 | unzip pairS = 142 | (fst <~ pairS, snd <~ pairS) 143 | 144 | 145 | {-| -} 146 | unzip3 : Signal (a,b,c) -> (Signal a, Signal b, Signal c) 147 | unzip3 pairS = 148 | ((\(a,_,_) -> a) <~ pairS, (\(_,b,_) -> b) <~ pairS, (\(_,_,c) -> c) <~ pairS) 149 | 150 | 151 | {-| -} 152 | unzip4 : Signal (a,b,c,d) -> (Signal a, Signal b, Signal c, Signal d) 153 | unzip4 pairS = 154 | ((\(a,_,_,_) -> a) <~ pairS, (\(_,b,_,_) -> b) <~ pairS, (\(_,_,c,_) -> c) <~ pairS, (\(_,_,_,d) -> d) <~ pairS) 155 | 156 | 157 | {-| Drops all updates to a signal, keeps only the initial value. 158 | -} 159 | initSignal : Signal a -> Signal a 160 | initSignal s = 161 | sampleOn (constant ()) s 162 | 163 | 164 | {-| `foldp'` is slighty more general than `foldp` in that you can base 165 | the initial value of the state on the initial value of the input value. 166 | 167 | foldp f b s == foldp' f (always b) s 168 | -} 169 | foldp' : (a -> b -> b) -> (a -> b) -> Signal a -> Signal b 170 | foldp' fun initFun input = 171 | let -- initial has no events, only the initial value is used 172 | initial = initSignal input ~> initFun 173 | -- both the initial value and the normal input are given to fun' 174 | rest = foldp fun' Nothing (zip input initial) 175 | -- when mb is Nothing, input had its first event to use ini 176 | -- otherwise use the b from Just 177 | fun' (inp, ini) mb = 178 | Maybe.withDefault ini mb 179 | |> fun inp |> Just 180 | 181 | in 182 | unsafeFromJust <~ merge (Just <~ initial) rest 183 | 184 | 185 | {-| Like `foldp`, but with a hidden state 186 | 187 | foldp f b i == 188 | let d a = (a,a) -- doubling function 189 | in foldps (\a b -> f a b |> d) (d b) i 190 | -} 191 | foldps : (a -> s -> (b,s)) -> (b,s) -> Signal a -> Signal b 192 | foldps f bs aS = 193 | fst <~ foldp (\a (_,s) -> f a s) bs aS 194 | 195 | 196 | {-| Like `foldp'`, but with a hidden state 197 | -} 198 | foldps' : (a -> s -> (b,s)) -> (a -> (b,s)) -> Signal a -> Signal b 199 | foldps' f iF aS = fst <~ foldp' (\a (_,s) -> f a s) iF aS 200 | 201 | 202 | {-| Not sure if this is useful for people, but it's a convenient building block: 203 | 204 | foldps == foldpWith identity 205 | foldp f b == 206 | let d a = (a,a) 207 | in foldpWith d f (d b) 208 | -} 209 | foldpWith : (h -> (b,s)) -> (a -> s -> h) -> (b,s) -> Signal a -> Signal b 210 | foldpWith unpack step init input = 211 | let 212 | step' a (_,s) = -- : a -> (b,s) -> (b,s) 213 | step a s |> unpack 214 | in 215 | foldp step' init input ~> fst 216 | 217 | 218 | {-| A running buffer of the given size (`n`) of the given signal. 219 | The list of at most `n` of the last values on the input signal. Starts 220 | with an empty list. Adds new values to the *end* of the list! So you get 221 | a list with time going from left to right. 222 | 223 | ((==) [1,2,3,4,5]) <~ runBuffer 5 (count (Time.every second)) 224 | -} 225 | runBuffer : Int -> Signal a -> Signal (List a) 226 | runBuffer = runBuffer' [] 227 | 228 | {-| Same as `runBuffer` but with an initial buffer. 229 | -} 230 | runBuffer' : List a -> Int -> Signal a -> Signal (List a) 231 | runBuffer' l n input = 232 | let 233 | f inp prev = 234 | let 235 | l = List.length prev 236 | in 237 | if l < n 238 | then prev ++ [inp] 239 | else List.drop (l-n+1) prev ++ [inp] 240 | in 241 | foldp f l input 242 | 243 | 244 | {-| A signal of each change to the provided signal, as a tuple of old 245 | and new values. 246 | 247 | The initial value of the tuple is the initial value of the provided signal, 248 | duplicated. Thereafter, the first part of the tuple is the old value of the 249 | provided signal, and the second part is the new value. 250 | -} 251 | deltas : Signal a -> Signal (a, a) 252 | deltas signal = 253 | let 254 | step value delta = 255 | (snd delta, value) 256 | 257 | initial value = 258 | (value, value) 259 | 260 | in 261 | foldp' step initial signal 262 | 263 | 264 | {-| Instead of delaying for some amount of time, delay for one round, 265 | where a round is initiated by outside event to the Elm program. 266 | This may not be be very useful yet. Let the package author know if you 267 | find a good use! 268 | Also known to `delay` in E-FRP. 269 | -} 270 | delayRound : b -> Signal b -> Signal b 271 | delayRound b bS = 272 | foldps (\new old -> (old, new)) (b,b) bS 273 | 274 | 275 | {-| Switch between two signals. When the first signal is `True`, use the 276 | second signal, otherwise use the third. 277 | -} 278 | switchWhen : Signal Bool -> Signal a -> Signal a -> Signal a 279 | switchWhen b l r = 280 | switchHelper keepWhen b l r 281 | 282 | 283 | {-| Same as the previous, but samples the signal it switches to. -} 284 | switchSample : Signal Bool -> Signal a -> Signal a -> Signal a 285 | switchSample b l r = 286 | switchHelper sampleWhen b l r 287 | 288 | 289 | switchHelper : (Signal Bool -> Maybe a -> Signal (Maybe a) -> Signal (Maybe a)) 290 | -> Signal Bool -> Signal a -> Signal a -> Signal a 291 | switchHelper filter b l r = 292 | let 293 | base = 294 | (\bi li ri -> Just <| if bi then li else ri) 295 | <~ initSignal b 296 | ~ initSignal l 297 | ~ initSignal r 298 | 299 | lAndR = 300 | merge 301 | (filter b Nothing (Just <~ l)) 302 | (filter (not <~ b) Nothing (Just <~ r)) 303 | 304 | in 305 | unsafeFromJust <~ merge base lAndR 306 | 307 | {-| The old name for `Signal.filter`, which doesn't confuse you with what the `Bool` value means. 308 | -} 309 | keepIf : (a -> Bool) -> a -> Signal a -> Signal a 310 | keepIf = Signal.filter 311 | 312 | {-| The good old `keepWhen` filter that keeps events from the `Signal a` as long as the 313 | `Signal Bool` is true. 314 | -} 315 | keepWhen : Signal Bool -> a -> Signal a -> Signal a 316 | keepWhen boolSig a aSig = 317 | zip boolSig aSig 318 | |> sampleOn aSig 319 | |> keepIf fst (True, a) 320 | |> map snd 321 | 322 | {-| A combination of `Signal.sampleOn` and `keepWhen`. When the 323 | first signal becomes `True`, the most recent value of the second signal 324 | will be propagated. 325 | [Before Elm 0.12]( 326 | https://github.com/elm-lang/elm-compiler/blob/master/changelog.md#012) 327 | this was the standard behaviour of `keepWhen`. 328 | -} 329 | sampleWhen : Signal Bool -> a -> Signal a -> Signal a 330 | sampleWhen bs def sig = 331 | zip bs sig 332 | |> keepIf fst (True, def) 333 | |> map snd 334 | 335 | 336 | {-| Like `keepWhen`, but when the filter signal turn `False`, the output 337 | changes back to the base value. 338 | -} 339 | keepThen : Signal Bool -> a -> Signal a -> Signal a 340 | keepThen choice base signal = 341 | switchSample choice signal <| constant base 342 | 343 | {-| `keepWhen` but always keeps the initial value rather than trying to 344 | filter it. 345 | -} 346 | keepWhenI : Signal Bool -> Signal a -> Signal a 347 | keepWhenI fs s = 348 | keepWhen (merge (constant True) fs) Nothing (Just <~ s) ~> unsafeFromJust 349 | 350 | 351 | {-| Filter a signal of optional values, discarding `Nothing`s. 352 | -} 353 | filter : a -> Signal (Maybe a) -> Signal a 354 | filter initial = filterMap identity initial 355 | 356 | 357 | {-| Apply a fold that may fail, ignore any non-changes. 358 | -} 359 | filterFold : (a -> b -> Maybe b) -> b -> Signal a -> Signal b 360 | filterFold f initial = 361 | let 362 | f' a s = 363 | let 364 | res = f a s 365 | in 366 | (res, Maybe.withDefault s res) 367 | in 368 | foldps f' (Just initial,initial) 369 | >> filter initial 370 | -- if it was a (a -> Maybe (b -> b)), the implementation would have been easier: 371 | -- filterFold f initial input = filterMap f identity input |> foldp (<|) initial 372 | 373 | 374 | {-| A function that merges the events of two signals without bias 375 | (unlike `Signal.merge`). It takes a resolution function for the 376 | (usually rare) case that the signals update in the same "round". 377 | 378 | fairMerge (\l r -> l) == merge 379 | -} 380 | fairMerge : (a -> a -> a) -> Signal a -> Signal a -> Signal a 381 | fairMerge resolve left right = 382 | let 383 | boolLeft = always True <~ left 384 | boolRight = always False <~ right 385 | bothUpdated = (/=) <~ (merge boolLeft boolRight) ~ (merge boolRight boolLeft) 386 | 387 | keep = keepWhenI bothUpdated 388 | resolved = resolve <~ keep left ~ keep right 389 | merged = merge left right 390 | in 391 | merged |> merge resolved 392 | 393 | {-| Merge each Signal in the given list into a given original Signal. This works 394 | like Signal.mergeMany, except that it does not crash when given an empty list. 395 | 396 | type Update = 397 | MouseMove (Int,Int) | TimeDelta Float | Click 398 | 399 | 400 | updates : Signal Update 401 | updates = 402 | mergeMany 403 | (MouseMove Mouse.position) 404 | [ map TimeDelta (fps 40) 405 | , map (always Click) Mouse.clicks 406 | ] 407 | -} 408 | mergeMany : Signal a -> List (Signal a) -> Signal a 409 | mergeMany original others = 410 | List.foldl Signal.merge original others 411 | 412 | 413 | {-| Combine a list of signals into a signal of lists. We have 414 | 415 | combine = mapMany identity 416 | 417 | Also, whenever you are in a situation where you write something like 418 | 419 | Signal.map f (combine signals) 420 | 421 | you are better off directly using `mapMany f signals`. -} 422 | combine : List (Signal a) -> Signal (List a) 423 | combine = List.foldr (map2 (::)) (constant []) 424 | 425 | 426 | {-| Apply a function to the current value of many signals. The 427 | function is reevaluated whenever any signal changes. A typical use case: 428 | 429 | mapMany (flow down) [sig_elem1, sig_elem2, sig_elem3] 430 | 431 | Note how this is nicer (and more extendable) than the equivalent: 432 | 433 | Signal.map3 (\e1 e2 e3 -> flow down [e1, e2, e3]) sig_elem1 sig_elem2 sig_elem3 434 | 435 | Also, `mapMany List.maximum : List (Signal comparable) -> Signal (Maybe comparable)` 436 | gives a signal that always carries `Just` the maximum value from all its 437 | input signals, unless the input signal list is empty, in which case the returned 438 | signal is equivalent to `constant Nothing`. 439 | -} 440 | mapMany : (List a -> b) -> List (Signal a) -> Signal b 441 | mapMany f l = 442 | f <~ combine l 443 | 444 | 445 | {-| Apply functions in a signal to the current value of many signals. 446 | The result is reevaluated whenever any signal changes. 447 | -} 448 | applyMany : Signal (List a -> b) -> List (Signal a) -> Signal b 449 | applyMany fs l = 450 | fs ~ combine l 451 | 452 | 453 | {-| Apply a function to the current value of two signals. The second signal is 454 | mapped passively -- that is, changes to the second signal do not force the 455 | function to re-evaluate. However, when the first signal changes, the function 456 | is re-evaluated with the current value of both signals. 457 | 458 | This is equivalent to Signal.map2, except that Signal.map2 re-evaluates the 459 | function when either Signal changes. 460 | -} 461 | passiveMap2 : (a -> b -> result) -> Signal a -> Signal b -> Signal result 462 | passiveMap2 func a = 463 | Signal.map2 func a << Signal.sampleOn a 464 | 465 | 466 | {-| Intended to be paired with Signal's `(<~)` operator, `withPassive` makes it 467 | possible for many signals to be passively mapped. For example, the 468 | following two declarations are equivalent: 469 | 470 | main : Signal Element 471 | main = 472 | scene <~ Mouse.position `withPassive` Window.dimensions 473 | 474 | main : Signal Element 475 | main = 476 | passiveMap2 scene Mouse.position Window.dimensions 477 | 478 | You can use this pattern to passively map as many signals as you want, by using 479 | `withPassive` many times. 480 | 481 | The function will only be re-evaluated when the signal mapped with `(<~)` 482 | changes. This is unlike the (otherwise equivalent) Signal `(~)` operator, since 483 | that operator re-evaluates the function whenever any of the input signals change. 484 | 485 | If you want the function to be re-evaluated when some signals change but not 486 | others, then you can combine the Signal `(~)` operator and `withPassive`, putting 487 | `(~)` first. For instance: 488 | 489 | main : Signal Element 490 | main = 491 | scene <~ Mouse.position ~ Window.dimensions `withPassive` anotherSignal 492 | 493 | In this example, the `scene` function will take three parameters, and will be called 494 | whenever either of the first two parameters changes. The third parameter will 495 | be the value of `anotherSignal`, but changes to `anotherSignal` will not cause 496 | the function to be re-evaluated. 497 | -} 498 | withPassive : Signal (a -> b) -> Signal a -> Signal b 499 | withPassive = 500 | passiveMap2 (<|) 501 | 502 | 503 | -- Give `withPassive` the same precedence as (~) so that it composes well 504 | infixl 4 `withPassive` 505 | 506 | 507 | -- Utility function for cases where we know we'll have a Just 508 | unsafeFromJust : Maybe a -> a 509 | unsafeFromJust maybe = 510 | case maybe of 511 | Just value -> 512 | value 513 | 514 | Nothing -> 515 | Debug.crash "This case should have been unreachable" 516 | -------------------------------------------------------------------------------- /src/Signal/Fun.elm: -------------------------------------------------------------------------------- 1 | module Signal.Fun 2 | ( scan 3 | , premap 4 | , postmap 5 | , bimap 6 | ) where 7 | {-| Some utility functions for signals of functions. The question is: 8 | Why are you dealing with signals of functions? Are you sure you want 9 | this? It can be used to harm testability and separation of concerns, 10 | which are endangered species and it's illegal to harm them! So please be 11 | careful. 12 | 13 | @docs scan, premap, postmap, bimap 14 | -} 15 | 16 | import Signal exposing (Signal) 17 | 18 | {-| Just a shorthand for signals of functions 19 | -} 20 | type alias SF a b = Signal (a -> b) 21 | 22 | {-| Takes a starting value for a state; applies each new function coming 23 | in to calculate the new state. 24 | -} 25 | scan : a -> SF a a -> Signal a 26 | scan = Signal.foldp (<|) 27 | 28 | {-| Compose the given function before every function that comes through 29 | the signal. 30 | -} 31 | premap : (a -> b) -> SF b c -> SF a c 32 | premap f1 = Signal.map (\fs -> f1 >> fs) 33 | 34 | {-| Compose the given function after every function that comes through 35 | the signal. 36 | -} 37 | postmap : (c -> d) -> SF b c -> SF b d 38 | postmap f2 = Signal.map (\fs -> fs >> f2) 39 | 40 | {-| Compose the given functions before and after every function that 41 | comes through the signal respectively. 42 | -} 43 | bimap : (a -> b) -> (c -> d) -> SF b c -> SF a d 44 | bimap f1 f2 = Signal.map (\fs -> f1 >> fs >> f2) 45 | -------------------------------------------------------------------------------- /src/Signal/Stream.elm: -------------------------------------------------------------------------------- 1 | module Signal.Stream(Stream, map, fairMerge, merge, mergeMany, fold, filterMap, filter, keepIf, sample, never, timestamp, toSignal, fromSignal) where 2 | 3 | {-| Uninitialised signals, that only give updates and don't have the concept of 4 | a current value. Like `Signal.Event.EventStream` (also in this package), only with values. 5 | 6 | @docs Stream 7 | 8 | This library provides the basic building blocks for routing these streams of 9 | events to your application logic. 10 | 11 | # Mapping 12 | @docs map 13 | 14 | # Merging 15 | @docs merge, fairMerge, mergeMany 16 | 17 | # Folding 18 | @docs fold 19 | 20 | # Filtering 21 | @docs filter, filterMap, keepIf, sample 22 | 23 | # Primitive Streams 24 | @docs never, timestamp 25 | 26 | # Conversions 27 | @docs toSignal, fromSignal 28 | -} 29 | 30 | import Signal exposing (Signal) 31 | import Signal.Extra as SignalE exposing ((~>), (<~)) 32 | import Signal.Time as SignalT exposing (Time) 33 | import Maybe exposing (Maybe(..)) 34 | import Debug 35 | 36 | {-| Streams of events. Many interactions with the world can be formulated as 37 | a stream of discrete events: mouse clicks, responses from servers, key presses, 38 | etc. 39 | -} 40 | type alias Stream a = Signal (Maybe a) 41 | 42 | -- Helper function, unsafe because 43 | fromJust : Maybe a -> a 44 | fromJust m = 45 | case m of 46 | Just a -> a 47 | Nothing -> 48 | Debug.crash <| "There was an implementation error somewhere in " 49 | ++ "Signal.Stream. If you're using the latest version of " 50 | ++ "Apanatshka/elm-signal-extra, please file an issue (if there isn't " 51 | ++ "such an issue yet). " 52 | 53 | maybeMap2 : (a -> b -> c) -> Maybe a -> Maybe b -> Maybe c 54 | maybeMap2 f l r = 55 | case (l,r) of 56 | (Just a, Just b) -> Just (f a b) 57 | _ -> Nothing 58 | 59 | {-| Apply a function to events as they come in. This lets you transform 60 | streams. 61 | 62 | type Action = MouseClick | TimeDelta Float 63 | 64 | actions : Stream Action 65 | actions = 66 | map (always MouseClick) Mouse.clicks 67 | -} 68 | map : (a -> b) -> Stream a -> Stream b 69 | map f = 70 | Signal.map (Maybe.map f) 71 | 72 | {-| Convert a stream of values into a signal that updates whenever an event 73 | comes in on the stream. 74 | 75 | url : Signal String 76 | 77 | url = 78 | toSignal "waiting.gif" imageStream 79 | 80 | constant : a -> Signal a 81 | 82 | constant value = 83 | toSignal value Stream.never 84 | -} 85 | toSignal : a -> Stream a -> Signal a 86 | toSignal a str = 87 | Signal.merge (Signal.constant <| Just a) str ~> fromJust 88 | 89 | {-| Ignore all updates to a signal, so it becomes the constant initial value. 90 | -} 91 | init : Signal a -> Signal a 92 | init sig = Signal.sampleOn (Signal.constant ()) sig 93 | 94 | {-| Get a stream that triggers whenever the signal is *updated*. Note 95 | that an update may result in the same value as before, so the resulting 96 | `Stream` can have the same value twice in a row. 97 | 98 | moves : Stream (Int,Int) 99 | moves = 100 | fromSignal Mouse.position 101 | -} 102 | fromSignal : Signal a -> Stream a 103 | fromSignal sig = 104 | Signal.merge (Signal.constant Nothing) (Just <~ sig) 105 | 106 | {-| Like `merge`, but you get to decide which event wins when they come in at the same time. 107 | -} 108 | fairMerge : (a -> a -> a) -> Stream a -> Stream a -> Stream a 109 | fairMerge f = 110 | SignalE.fairMerge (maybeMap2 f) 111 | 112 | {-| Merge two streams into one. This function is extremely useful for bringing 113 | together lots of different streams to feed into a `fold`. 114 | 115 | type Action = MouseClick | TimeDelta Float 116 | 117 | actions : Stream Action 118 | actions = 119 | merge 120 | (map (always MouseClick) Mouse.clicks) 121 | (map TimeDelta (fps 40)) 122 | 123 | If an event comes from either of the incoming streams, it flows out the 124 | outgoing stream. If an event comes on both streams at the same time, the left 125 | event wins (i.e., the right event is discarded). 126 | -} 127 | merge : Stream a -> Stream a -> Stream a 128 | merge = 129 | fairMerge (\l _ -> l) 130 | 131 | {-| Merge many streams into one. This is useful when you are merging more than 132 | two streams. When multiple events come in at the same time, the left-most 133 | event wins, just like with `merge`. 134 | 135 | type Action = MouseMove (Int,Int) | TimeDelta Float | Click 136 | 137 | actions : Stream Action 138 | actions = 139 | mergeMany 140 | [ map MouseMove Mouse.position 141 | , map TimeDelta (fps 40) 142 | , map (always Click) Mouse.clicks 143 | ] 144 | -} 145 | mergeMany : List (Stream a) -> Stream a 146 | mergeMany = 147 | List.foldr merge never 148 | 149 | {-| Create a past-dependent value. Each update from the incoming stream will 150 | be used to step the state forward. The outgoing signal represents the current 151 | state. 152 | 153 | clickCount : Signal Int 154 | clickCount = 155 | fold (\click total -> total + 1) 0 Mouse.clicks 156 | 157 | timeSoFar : Stream Time 158 | timeSoFar = 159 | fold (+) 0 (fps 40) 160 | 161 | So `clickCount` updates on each mouse click, incrementing by one. `timeSoFar` 162 | is the time the program has been running, updated 40 times a second. 163 | -} 164 | fold : (a -> b -> b) -> b -> Stream a -> Signal b 165 | fold f b str = 166 | Signal.foldp (f << fromJust) b str 167 | 168 | 169 | {-| Filter out some events. The given function decides whether we should 170 | keep an update. The following example only keeps even numbers. 171 | 172 | numbers : Stream Int 173 | 174 | isEven : Int -> Bool 175 | 176 | evens : Stream Int 177 | evens = 178 | keepIf isEven numbers 179 | -} 180 | keepIf : (a -> Bool) -> Stream a -> Stream a 181 | keepIf isOk stream = 182 | filterMap (\v -> if isOk v then Just v else Nothing) stream 183 | 184 | 185 | {-| Filter out some events. If the incoming event is mapped to a `Nothing` it 186 | is dropped. If it is mapped to `Just` a value, we keep the value. 187 | 188 | numbers : Stream Int 189 | numbers = 190 | filterMap (\raw -> Result.toMaybe (String.toInt raw)) userInput 191 | 192 | userInput : Stream String 193 | -} 194 | filterMap : (a -> Maybe b) -> Stream a -> Stream b 195 | filterMap f = 196 | map f >> filter 197 | 198 | {-| Filter out and unwrap the `Just`s in the stream. 199 | -} 200 | filter : Stream (Maybe a) -> Stream a 201 | filter str = SignalE.filter Nothing str 202 | 203 | {-| Useful for augmenting a stream with information from a signal. 204 | For example, if you are operating on a time delta but want to take the current 205 | keyboard state into account. 206 | 207 | inputs : Stream ({ x:Int, y:Int }, Time) 208 | inputs = 209 | sample (,) Keyboard.arrows (fps 60) 210 | 211 | Now we get events exactly with the `(fps 60)` stream, but they are augmented 212 | with which arrows are pressed at the moment. 213 | -} 214 | sample : (a -> b -> c) -> Signal a -> Stream b -> Stream c 215 | sample f signal events = 216 | Signal.map2 (maybeMap2 f) (Just <~ signal) events 217 | 218 | {-| A stream that never gets an update. This is useful when defining functions 219 | like `mergeMany` which needs to be defined even when no streams are given. 220 | 221 | mergeMany : List (Stream a) -> Stream a 222 | mergeMany streams = 223 | List.foldr merge never streams 224 | -} 225 | never : Stream a 226 | never = 227 | Signal.constant Nothing 228 | 229 | 230 | {-| Add a timestamp to any stream. Timestamps increase monotonically. When you 231 | create `(timestamp Mouse.x)`, an initial timestamp is produced. The timestamp 232 | updates whenever `Mouse.x` updates. 233 | 234 | Timestamp updates are tied to individual events, so 235 | `(timestamp Mouse.x)` and `(timestamp Mouse.y)` will always have the same 236 | timestamp because they rely on the same underlying event (`Mouse.position`). 237 | -} 238 | timestamp : Stream a -> Stream (Time, a) 239 | timestamp = 240 | SignalT.timestamp >> Signal.map (\(t,m) -> Maybe.map (\a -> (t,a)) m) 241 | -------------------------------------------------------------------------------- /src/Signal/Time.elm: -------------------------------------------------------------------------------- 1 | module Signal.Time 2 | ( Time 3 | , limitRate 4 | , dropWithin 5 | , settledAfter 6 | , startTime 7 | , relativeTime 8 | , since 9 | , delay 10 | , timestamp) where 11 | {-| Time related functions for `Signal`s. 12 | 13 | # Easy does it 14 | Controlling too frequently changing signals. 15 | @docs limitRate, dropWithin, settledAfter 16 | 17 | # Relative time 18 | @docs startTime, relativeTime 19 | 20 | # Re-exports 21 | Some functions from the `Time` module that fit in. 22 | @docs Time, since, delay, timestamp 23 | -} 24 | 25 | import Signal exposing (Signal) 26 | import Signal.Extra exposing ((~>), (~), (<~)) 27 | import Signal.Discrete as Discrete 28 | import Time 29 | 30 | {-| Just re-exporting the `Time` type from the `Time` module 31 | -} 32 | type alias Time = Time.Time 33 | 34 | {-| Keep only the timestamps 35 | -} 36 | timestamps : Signal a -> Signal Time 37 | timestamps s = 38 | timestamp s ~> fst 39 | 40 | 41 | {-| Limits the given signal to output a maximum of one message within 42 | the given time period. 43 | 44 | After an update of the given signal, for the given period subsequent 45 | updates are dropped. The original update that started this dropping is 46 | kept. 47 | 48 | throttledMouseClicks = limitRate 60 Mouse.clicks 49 | 50 | Also known in some areas as a `throttle` function. 51 | -} 52 | limitRate : Time -> Signal a -> Signal a 53 | limitRate period sig = 54 | let 55 | within newt oldt = 56 | if newt - oldt > period 57 | then newt 58 | else oldt 59 | windowStart = timestamps sig |> Signal.foldp within 0 60 | in 61 | Signal.sampleOn (Discrete.whenChange windowStart) sig 62 | 63 | 64 | {-| Drops all but the first update of a flurry of updates (a stutter). 65 | The stutter is defined as updates that happen with max. the given time 66 | in between. 67 | 68 | The first update of the given signal is sent through. Then the given 69 | delay is waited. If no other updates arrive during that time, then next 70 | update will be sent through. Any update that arrives within the given 71 | time of the last update is dropped. 72 | 73 | noDoubleClicks = dropWithin (300 * milliseconds) Mouse.clicks 74 | 75 | Also known to some areas as an "immediate" `debounce` function. 76 | -} 77 | dropWithin : Time -> Signal a -> Signal a 78 | dropWithin delay sig = 79 | let 80 | leading = since delay sig |> Discrete.whenChangeTo True 81 | in 82 | Signal.sampleOn leading sig 83 | 84 | 85 | {-| Gives the last update of a flurry of updates (a stutter) after has 86 | settled* for the given time. The stutter is defined as updates that 87 | happen within the given time. * Where settled the signal gets no further 88 | updates for some time, it's **not** relating to the value changes of the 89 | signal. 90 | 91 | After every update of the given signal, the given delay is waited. If no 92 | other updates arrived during that time, the update is sent through. If a 93 | new update arrives within the given time, the previous update is dropped 94 | and the waiting is restarted. So `debounce`-ing a signal that keeps up 95 | the flurry of updates all the time results in a signal that never 96 | updates. 97 | 98 | tooltip : Signal Bool 99 | tooltip = 100 | merge (always False <~ Mouse.position) 101 | (always True <~ (Mouse.position 102 | |> settledAfter (500 * Time.millisecond))) 103 | 104 | Also known in some areas as a `debounce` function. 105 | -} 106 | settledAfter : Time -> Signal a -> Signal a 107 | settledAfter delay sig = 108 | let 109 | trailing = since delay sig |> Discrete.whenChangeTo False 110 | in 111 | Signal.sampleOn trailing sig 112 | 113 | 114 | {-| The timestamp of the start of the program. 115 | -} 116 | startTime : Signal Time 117 | startTime = Signal.constant () |> timestamps 118 | 119 | 120 | {-| Turns absolute time signal to time relative to the start of the 121 | program. 122 | 123 | let tick = Time.every Time.second 124 | in Signal.foldp ((+) 1) 0 tick == 125 | relativeTime tick ~> Time.inSeconds >> round 126 | -} 127 | relativeTime : Signal Time -> Signal Time 128 | relativeTime s = 129 | (-) <~ s ~ startTime 130 | 131 | 132 | {-| A re-export of [Time.since](http://package.elm-lang.org/packages/elm-lang/core/1.0.0/Time#since). 133 | 134 | Takes a time `t` and any signal. The resulting boolean signal is true 135 | for time `t` after every event on the input signal. So ``(second `since` 136 | Mouse.clicks)`` would result in a signal that is true for one second 137 | after each mouse click and false otherwise. 138 | -} 139 | since : Time -> Signal a -> Signal Bool 140 | since = Time.since 141 | 142 | {-| A re-export of [Time.delay](http://package.elm-lang.org/packages/elm-lang/core/1.0.0/Time#delay). 143 | 144 | Delay a signal by a certain amount of time. So `(delay second 145 | Mouse.clicks)` will update one second later than any mouse click. 146 | -} 147 | delay : Time -> Signal a -> Signal a 148 | delay = Time.delay 149 | 150 | 151 | {-| A re-export of [Time.timestamp](http://package.elm-lang.org/packages/elm-lang/core/1.0.0/Time#timestamp). 152 | 153 | Add a timestamp to any signal. Timestamps increase monotonically. When 154 | you create `(timestamp Mouse.x)`, an initial timestamp is produced. The 155 | timestamp updates whenever `Mouse.x` updates. 156 | 157 | Timestamp updates are tied to individual events, so `(timestamp 158 | Mouse.x)` and `(timestamp Mouse.y)` will always have the same timestamp 159 | because they rely on the same underlying event (`Mouse.position`). 160 | -} 161 | timestamp : Signal a -> Signal (Time, a) 162 | timestamp = Time.timestamp 163 | -------------------------------------------------------------------------------- /test/Native/ApanatshkaSignalExtra.js: -------------------------------------------------------------------------------- 1 | Elm.Native.ApanatshkaSignalExtra = {}; 2 | Elm.Native.ApanatshkaSignalExtra.make = function (localRuntime) { 3 | localRuntime.Native = localRuntime.Native || {}; 4 | localRuntime.Native.ApanatshkaSignalExtra = localRuntime.Native.ApanatshkaSignalExtra || {}; 5 | 6 | if (!localRuntime.Native.ApanatshkaSignalExtra.values) { 7 | var Task = Elm.Native.Task.make(localRuntime); 8 | var Signal = Elm.Native.Signal.make(localRuntime); 9 | 10 | var sample = function (signal) { 11 | // Use closure to track value 12 | var val = signal.value; 13 | 14 | var handler = function (value) { 15 | val = value; 16 | }; 17 | 18 | // We construct a new "output" node, because otherwise the incoming 19 | // signal may be pruned by trimDeadNodes() in Runtime.js 20 | // (if trimDeadNodes() sees that it is not otherwise used). 21 | var output = Signal.output("sample-" + signal.name, handler, signal); 22 | 23 | return Task.asyncFunction(function (callback) { 24 | // Need to return the value inside setTimeout, because 25 | // otherwise we can be called out-of-order ... that is, a 26 | // previous `Task.andThen` which updated a Signal may not have 27 | // actually completed yet unless we do this inside a timeout. 28 | localRuntime.setTimeout(function () { 29 | callback(Task.succeed(val)); 30 | }, 0); 31 | }); 32 | }; 33 | 34 | localRuntime.Native.ApanatshkaSignalExtra.values = { 35 | sample: sample 36 | }; 37 | } 38 | 39 | return localRuntime.Native.ApanatshkaSignalExtra.values; 40 | }; 41 | 42 | -------------------------------------------------------------------------------- /test/Signal/ExtraTest.elm: -------------------------------------------------------------------------------- 1 | module Signal.ExtraTest where 2 | 3 | import Signal exposing (Mailbox, mailbox, send, foldp, sampleOn) 4 | import Task exposing (Task, sequence, andThen) 5 | import Signal.Extra exposing (passiveMap2, withPassive, mapMany, andMap, deltas, (~), (<~)) 6 | import ElmTest.Test exposing (..) 7 | import ElmTest.Assertion exposing (..) 8 | import Native.ApanatshkaSignalExtra 9 | 10 | 11 | -- A convenience for the testing DSL ... see use below ... 12 | -- lets you do some andThens and then map last 13 | (>>>) = 14 | flip Task.map 15 | 16 | -- Basically, an `andThen` which ignores its argument 17 | (>>-) task func = 18 | task `andThen` (always func) 19 | 20 | 21 | infixl 4 >>> 22 | infixl 4 >>- 23 | 24 | 25 | {-| Construct a task which, when performed, will return the current value of a Signal. -} 26 | sample : Signal a -> Task x a 27 | sample = 28 | Native.ApanatshkaSignalExtra.sample 29 | 30 | 31 | signalToSample = mailbox 0 32 | 33 | sampleTest : Task x Test 34 | sampleTest = 35 | send signalToSample.address 5 36 | >>- sample signalToSample.signal 37 | >>> assertEqual 5 >> test "should sample value" 38 | 39 | 40 | -- Some mapping functions 41 | consume2 : Int -> Int -> List Int 42 | consume2 a b = 43 | [a, b] 44 | 45 | 46 | consume4 : Int -> Int -> Int -> Int -> List Int 47 | consume4 a b c d = 48 | [a, b, c, d] 49 | 50 | 51 | signal1 = mailbox 0 52 | signal2 = mailbox 0 53 | 54 | -- We're testing the behaviour of a Signal, which is a little tricky, 55 | -- since you want to make updates to the Signal and then see what 56 | -- happens. 'Making updates to the signal' is essentially a matter 57 | -- for Tasks -- that is, you stimulate a Signal via a Task. You can 58 | -- then use the `sample` function above to get the value of a Signal, 59 | -- and then use that to construct a test. So you end up with a Task 60 | -- that eventually produces a Test. Once you've got that, you just 61 | -- need to bundle them together and 'run' them. 62 | passiveMap2OnlyFiresOnSignalA : Task x Test 63 | passiveMap2OnlyFiresOnSignalA = 64 | let 65 | signal = 66 | passiveMap2 consume2 signal1.signal signal2.signal 67 | 68 | counter = 69 | foldp (\_ s -> s + 1) 0 signal 70 | 71 | in 72 | sequence ( 73 | List.map (\int -> 74 | send signal1.address int 75 | >>- send signal2.address int 76 | ) [1 .. 5] 77 | ) 78 | >>- sample counter 79 | >>> assertEqual 5 >> test "passiveMap2 fires only for first signal" 80 | 81 | 82 | signal3 = mailbox 0 83 | signal4 = mailbox 0 84 | 85 | passiveMap2ActuallySamples : Task x Test 86 | passiveMap2ActuallySamples = 87 | let 88 | signal = 89 | passiveMap2 consume2 signal3.signal signal4.signal 90 | 91 | in 92 | send signal4.address 25 93 | >>- send signal3.address 26 94 | >>- sample signal 95 | >>> assertEqual [26, 25] >> test "passiveMap2 actually samples" 96 | 97 | 98 | signal5 = mailbox 0 99 | signal6 = mailbox 0 100 | signal7 = mailbox 0 101 | signal8 = mailbox 0 102 | 103 | complicatedMappingFiresCorrectly : Task x Test 104 | complicatedMappingFiresCorrectly = 105 | let 106 | signal = 107 | consume4 108 | <~ signal5.signal 109 | ~ signal6.signal 110 | `withPassive` signal7.signal 111 | `withPassive` signal8.signal 112 | 113 | counter = 114 | foldp (\_ s -> s + 1) 0 signal 115 | 116 | in 117 | sequence ( 118 | List.map (\int -> 119 | send signal5.address int 120 | >>- send signal6.address int 121 | >>- send signal7.address int 122 | >>- send signal8.address int 123 | ) [1 .. 5] 124 | ) 125 | >>- sample counter 126 | >>> assertEqual 10 >> test "complicated signal fires only for first two signals" 127 | 128 | 129 | signal9 = mailbox 0 130 | signal10 = mailbox 0 131 | signal11 = mailbox 0 132 | signal12 = mailbox 0 133 | 134 | complicatedMappingActuallySamples : Task x Test 135 | complicatedMappingActuallySamples = 136 | let 137 | signal = 138 | consume4 139 | <~ signal9.signal 140 | ~ signal10.signal 141 | `withPassive` signal11.signal 142 | `withPassive` signal12.signal 143 | 144 | in 145 | send signal12.address 26 146 | >>- send signal11.address 27 147 | >>- send signal10.address 28 148 | >>- send signal9.address 29 149 | >>- sample signal 150 | >>> assertEqual [29, 28, 27, 26] >> test "passiveMap2 actually samples" 151 | 152 | 153 | type alias User = 154 | { name : String 155 | , age : Int 156 | } 157 | 158 | signal13 = mailbox "" 159 | signal14 = mailbox 0 160 | 161 | andMapAppliesSignalFunctionToSignal : Task x Test 162 | andMapAppliesSignalFunctionToSignal = 163 | let 164 | signal = User 165 | `Signal.map` signal13.signal 166 | `andMap` signal14.signal 167 | in 168 | send signal13.address "Bobby" 169 | >>- send signal14.address 5 170 | >>- sample signal 171 | >>> assertEqual { name = "Bobby", age = 5 } 172 | >> test "andMap applies fn in first signal to result of second signal" 173 | 174 | 175 | signal15 = mailbox ((+) 10) 176 | signal16 = mailbox 0 177 | 178 | andMapAppliesFunctionIfItUpdates : Task x Test 179 | andMapAppliesFunctionIfItUpdates = 180 | let 181 | signal = 182 | signal15.signal 183 | `andMap` signal16.signal 184 | in 185 | send signal15.address ((+) 20) 186 | >>- sample signal 187 | >>> assertEqual 20 188 | >> test "andMap applies fn in first signal whenever it changes" 189 | 190 | 191 | signal17 = mailbox ((+) 10) 192 | signal18 = mailbox 0 193 | 194 | andMapAppliesFunctionIfValueSignalUpdates : Task x Test 195 | andMapAppliesFunctionIfValueSignalUpdates = 196 | let 197 | signal = 198 | signal17.signal 199 | `andMap` signal18.signal 200 | in 201 | send signal18.address 10 202 | >>- sample signal 203 | >>> assertEqual 20 204 | >> test "andMap applies fn in first signal to the value of the second when it changes" 205 | 206 | 207 | deltasInitialValue : Task x Test 208 | deltasInitialValue = 209 | let 210 | mbox = 211 | mailbox 0 212 | 213 | signal = 214 | deltas mbox.signal 215 | 216 | in 217 | sample signal 218 | >>> assertEqual (0, 0) 219 | >> test "deltas should start with the initial value of the signal duplicated" 220 | 221 | 222 | deltasOneUpdate : Task x Test 223 | deltasOneUpdate = 224 | let 225 | mbox = 226 | mailbox 0 227 | 228 | signal = 229 | deltas mbox.signal 230 | 231 | in 232 | send mbox.address 1 233 | >>- sample signal 234 | >>> assertEqual (0, 1) 235 | >> test "the first update to deltas should work" 236 | 237 | 238 | deltasTwoUpdates : Task x Test 239 | deltasTwoUpdates = 240 | let 241 | mbox = 242 | mailbox 0 243 | 244 | signal = 245 | deltas mbox.signal 246 | 247 | in 248 | send mbox.address 1 249 | >>- send mbox.address 2 250 | >>- sample signal 251 | >>> assertEqual (1, 2) 252 | >> test "the second update to deltas should work" 253 | 254 | 255 | tests : Task x Test 256 | tests = 257 | sequence 258 | [ sampleTest 259 | , passiveMap2OnlyFiresOnSignalA 260 | , passiveMap2ActuallySamples 261 | , complicatedMappingFiresCorrectly 262 | , complicatedMappingActuallySamples 263 | , andMapAppliesSignalFunctionToSignal 264 | , andMapAppliesFunctionIfItUpdates 265 | , andMapAppliesFunctionIfValueSignalUpdates 266 | , deltasInitialValue 267 | , deltasOneUpdate 268 | , deltasTwoUpdates 269 | ] 270 | >>> suite "Signal.Extra tests" 271 | -------------------------------------------------------------------------------- /test/Test.elm: -------------------------------------------------------------------------------- 1 | module Test where 2 | 3 | import Graphics.Element exposing (Element) 4 | import Signal exposing (Signal, Mailbox, mailbox, constant, send) 5 | import ElmTest.Assertion exposing (..) 6 | import ElmTest.Test exposing (..) 7 | import ElmTest.Runner.Element exposing (runDisplay) 8 | import Task exposing (Task, andThen, sequence, map) 9 | import Signal.ExtraTest 10 | import Signal.Extra exposing (mapMany) 11 | 12 | 13 | -- Testing signals is a bit tricky, since once you get into 14 | -- Signal-world, you can't really get out -- the most you can do 15 | -- is construct another Signal with a transformed type. Thus, we 16 | -- need to end up with a Signal of Elements, and working back from 17 | -- that, a Signal of Tests. 18 | main : Signal Element 19 | main = 20 | Signal.map runDisplay tests.signal 21 | 22 | 23 | tests : Mailbox Test 24 | tests = 25 | mailbox <| 26 | test 27 | "elm-signal-tests: initial state" 28 | (assert False) 29 | 30 | 31 | -- Note that the sequence is to permit additional tests to be defined 32 | -- easily ... that is, you can add to the list 33 | port tasks : Task x () 34 | port tasks = 35 | Task.map 36 | (suite "elm-signal-extra tests") 37 | (sequence 38 | [ Signal.ExtraTest.tests 39 | ] 40 | ) 41 | `andThen` send tests.address 42 | 43 | -------------------------------------------------------------------------------- /test/elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "5.2.1", 3 | "summary": "Tests for elm-signal-extra", 4 | "repository": "https://github.com/Apanatshka/elm-signal-extra.git", 5 | "license": "MIT", 6 | "source-directories": [ 7 | ".", 8 | "../src" 9 | ], 10 | "exposed-modules": [ 11 | ], 12 | "native-modules": true, 13 | "dependencies": { 14 | "elm-lang/core": "2.0.0 <= v < 3.0.0", 15 | "deadfoxygrandpa/elm-test": "2.0.0 <= v < 3.0.0" 16 | }, 17 | "elm-version": "0.16.0 <= v < 0.17.0" 18 | } 19 | --------------------------------------------------------------------------------