git.iko.soy/rowtype-yoga/purescript-abcjs
on main help sign in
Use EffectFn across all FFI; add Selector/Abc newtypes to kill positional String ambiguity
commit2864bfd4542ae3d2bacc558e94e1974897aa05fa authorMark <mark.eibes@gmail.com> dateSat 30 May 2026, 16:27 (4 months ago) parent9f36644
11 files changed+80−63
.spec-results+3−3 spago.lock+1 spago.yaml+1 src/Abcjs/Render.js+1−1 src/Abcjs/Render.purs+8−7 src/Abcjs/Synth.js+8−9 src/Abcjs/Synth.purs+19−19 src/Abcjs/Timing.js+6−6 src/Abcjs/Timing.purs+13−13 src/Abcjs/Types.purs+16−1 test/Test/Main.purs+4−4
.spec-results+3−3
@@ -2,14 +2,14 @@
22 [
33 "Abcjs.Render.parseOnly always yields exactly one tune object",
44 {
5− "timestamp": "1780150858418.0",
5+ "timestamp": "1780151218238.0",
66 "success": true
77 }
88 ],
99 [
1010 "Abcjs.Render.parseOnly parses a game-generated phrase to at least one line of music",
1111 {
12− "timestamp": "1780150858418.0",
12+ "timestamp": "1780151218238.0",
1313 "success": true
1414 }
1515 ],
@@ -23,7 +23,7 @@
2323 [
2424 "Abcjs.Render.parseOnly parses empty input to a tune with no music",
2525 {
26− "timestamp": "1780150858418.0",
26+ "timestamp": "1780151218238.0",
2727 "success": true
2828 }
2929 ],
spago.lock+1
@@ -9,6 +9,7 @@
99 "aff-promise",
1010 "arrays",
1111 "effect",
12+ "newtype",
1213 "nullable",
1314 "prelude"
1415 ]
spago.yaml+1
@@ -5,6 +5,7 @@ package:
55 - aff-promise
66 - arrays
77 - effect
8+ - newtype
89 - nullable
910 - prelude
1011 test:
src/Abcjs/Render.js+1−1
@@ -1,6 +1,6 @@
11import abcjs from "abcjs";
22
3−export const renderAbcImpl = (selector) => (abc) => (params) => () =>
3+export const renderAbcImpl = (selector, abc, params) =>
44 abcjs.renderAbc(selector, abc, params);
55
66export const parseOnlyImpl = (abc) => abcjs.parseOnly(abc);
src/Abcjs/Render.purs+8−7
@@ -5,8 +5,9 @@ module Abcjs.Render
55 , parseOnly
66 ) where
77
8+import Abcjs.Types (Abc, Selector, TuneObject)
89import Effect (Effect)
9−import Abcjs.Types (TuneObject)
10+import Effect.Uncurried (EffectFn3, runEffectFn3)
1011
1112-- | The subset of abcjs render options most relevant to a game.
1213-- | `responsive: "resize"` makes the SVG scale to its container,
@@ -26,16 +27,16 @@ defaultRenderParams =
2627 , add_classes: true
2728 }
2829
29−-- | Draw ABC notation into the element matched by a CSS selector (or id).
30+-- | Draw ABC notation into the element matched by a selector.
3031-- | Returns the rendered tunes to feed into the synth and timing APIs.
31−renderAbc :: String -> String -> RenderParams -> Effect (Array TuneObject)
32−renderAbc = renderAbcImpl
32+renderAbc :: Selector -> Abc -> RenderParams -> Effect (Array TuneObject)
33+renderAbc = runEffectFn3 renderAbcImpl
3334
34−foreign import renderAbcImpl :: String -> String -> RenderParams -> Effect (Array TuneObject)
35+foreign import renderAbcImpl :: EffectFn3 Selector Abc RenderParams (Array TuneObject)
3536
3637-- | Parse ABC without drawing anything — no DOM required.
3738-- | Useful for generating and inspecting phrases in game logic.
38−parseOnly :: String -> Array TuneObject
39+parseOnly :: Abc -> Array TuneObject
3940parseOnly = parseOnlyImpl
4041
41−foreign import parseOnlyImpl :: String -> Array TuneObject
42+foreign import parseOnlyImpl :: Abc -> Array TuneObject
src/Abcjs/Synth.js+8−9
@@ -4,22 +4,21 @@ export const supportsAudio = () => abcjs.synth.supportsAudio();
44
55export const newSynthController = () => new abcjs.synth.SynthController();
66
7−export const loadImpl = (selector) => (cursorControl) => (options) => (controller) => () =>
7+export const loadImpl = (selector, cursorControl, options, controller) =>
88 controller.load(selector, cursorControl, options);
99
10−export const setTuneImpl = (tune) => (userAction) => (controller) => () =>
10+export const setTuneImpl = (tune, userAction, controller) =>
1111 controller.setTune(tune, userAction);
1212
13−export const playImpl = (controller) => () => controller.play();
13+export const playImpl = (controller) => controller.play();
1414
15−export const pauseImpl = (controller) => () => controller.pause();
15+export const pauseImpl = (controller) => controller.pause();
1616
17−export const restartImpl = (controller) => () => controller.restart();
17+export const restartImpl = (controller) => controller.restart();
1818
19−export const seekSecondsImpl = (seconds) => (controller) => () =>
19+export const seekSecondsImpl = (seconds, controller) =>
2020 controller.seek(seconds, "seconds");
2121
22−export const setWarpImpl = (percent) => (controller) => () =>
23− controller.setWarp(percent);
22+export const setWarpImpl = (percent, controller) => controller.setWarp(percent);
2423
25−export const toggleLoopImpl = (controller) => () => controller.toggleLoop();
24+export const toggleLoopImpl = (controller) => controller.toggleLoop();
src/Abcjs/Synth.purs+19−19
@@ -17,11 +17,11 @@ module Abcjs.Synth
1717
1818import Prelude
1919
20−import Abcjs.Types (TimingEvent, TuneObject)
20+import Abcjs.Types (Selector, TimingEvent, TuneObject)
2121import Control.Promise (Promise, toAffE)
2222import Effect (Effect)
2323import Effect.Aff (Aff)
24−import Effect.Uncurried (EffectFn1, EffectFn3)
24+import Effect.Uncurried (EffectFn1, EffectFn2, EffectFn3, EffectFn4, runEffectFn1, runEffectFn2, runEffectFn3, runEffectFn4)
2525
2626-- | The high-level player: a transport plus an optional on-page control panel.
2727foreign import data SynthController :: Type
@@ -60,47 +60,47 @@ foreign import supportsAudio :: Effect Boolean
6060foreign import newSynthController :: Effect SynthController
6161
6262-- | Attach the controller to a control-panel element and a cursor control.
63−load :: String -> CursorControl -> ControlOptions -> SynthController -> Effect Unit
64−load = loadImpl
63+load :: Selector -> CursorControl -> ControlOptions -> SynthController -> Effect Unit
64+load = runEffectFn4 loadImpl
6565
66−foreign import loadImpl :: String -> CursorControl -> ControlOptions -> SynthController -> Effect Unit
66+foreign import loadImpl :: EffectFn4 Selector CursorControl ControlOptions SynthController Unit
6767
6868-- | Prime a tune for playback. The `Boolean` says whether this was triggered
6969-- | by a user gesture, which lets the browser unlock the audio context.
7070setTune :: TuneObject -> Boolean -> SynthController -> Aff Unit
71−setTune tune userAction controller = toAffE (setTuneImpl tune userAction controller)
71+setTune tune userAction controller = toAffE (runEffectFn3 setTuneImpl tune userAction controller)
7272
73−foreign import setTuneImpl :: TuneObject -> Boolean -> SynthController -> Effect (Promise Unit)
73+foreign import setTuneImpl :: EffectFn3 TuneObject Boolean SynthController (Promise Unit)
7474
7575-- | Toggle play/pause.
7676play :: SynthController -> Effect Unit
77−play = playImpl
77+play = runEffectFn1 playImpl
7878
79−foreign import playImpl :: SynthController -> Effect Unit
79+foreign import playImpl :: EffectFn1 SynthController Unit
8080
8181pause :: SynthController -> Effect Unit
82−pause = pauseImpl
82+pause = runEffectFn1 pauseImpl
8383
84−foreign import pauseImpl :: SynthController -> Effect Unit
84+foreign import pauseImpl :: EffectFn1 SynthController Unit
8585
8686restart :: SynthController -> Effect Unit
87−restart = restartImpl
87+restart = runEffectFn1 restartImpl
8888
89−foreign import restartImpl :: SynthController -> Effect Unit
89+foreign import restartImpl :: EffectFn1 SynthController Unit
9090
9191-- | Jump to a position given in seconds.
9292seekSeconds :: Number -> SynthController -> Effect Unit
93−seekSeconds = seekSecondsImpl
93+seekSeconds = runEffectFn2 seekSecondsImpl
9494
95−foreign import seekSecondsImpl :: Number -> SynthController -> Effect Unit
95+foreign import seekSecondsImpl :: EffectFn2 Number SynthController Unit
9696
9797-- | Set tempo as a percentage of the written tempo (100.0 = as written).
9898setWarp :: Number -> SynthController -> Effect Unit
99−setWarp = setWarpImpl
99+setWarp = runEffectFn2 setWarpImpl
100100
101−foreign import setWarpImpl :: Number -> SynthController -> Effect Unit
101+foreign import setWarpImpl :: EffectFn2 Number SynthController Unit
102102
103103toggleLoop :: SynthController -> Effect Unit
104−toggleLoop = toggleLoopImpl
104+toggleLoop = runEffectFn1 toggleLoopImpl
105105
106−foreign import toggleLoopImpl :: SynthController -> Effect Unit
106+foreign import toggleLoopImpl :: EffectFn1 SynthController Unit
src/Abcjs/Timing.js+6−6
@@ -1,14 +1,14 @@
11import abcjs from "abcjs";
22
3−export const newTimingCallbacksImpl = (visualObj) => (options) => () =>
3+export const newTimingCallbacksImpl = (visualObj, options) =>
44 new abcjs.TimingCallbacks(visualObj, options);
55
6−export const startImpl = (tc) => () => tc.start();
6+export const startImpl = (tc) => tc.start();
77
8−export const stopImpl = (tc) => () => tc.stop();
8+export const stopImpl = (tc) => tc.stop();
99
10−export const pauseImpl = (tc) => () => tc.pause();
10+export const pauseImpl = (tc) => tc.pause();
1111
12−export const resetImpl = (tc) => () => tc.reset();
12+export const resetImpl = (tc) => tc.reset();
1313
14−export const setProgressImpl = (percent) => (tc) => () => tc.setProgress(percent);
14+export const setProgressImpl = (percent, tc) => tc.setProgress(percent);
src/Abcjs/Timing.purs+13−13
@@ -14,7 +14,7 @@ import Prelude
1414import Abcjs.Types (TimingEvent, TuneObject)
1515import Data.Nullable (Nullable)
1616import Effect (Effect)
17−import Effect.Uncurried (EffectFn1, EffectFn3)
17+import Effect.Uncurried (EffectFn1, EffectFn2, EffectFn3, runEffectFn1, runEffectFn2)
1818
1919-- | A standalone animation timer over a rendered tune. Drives the cursor and
2020-- | game logic off note timing, independent of whether audio is playing.
@@ -30,32 +30,32 @@ type TimingOptions =
3030 }
3131
3232newTimingCallbacks :: TuneObject -> TimingOptions -> Effect TimingCallbacks
33−newTimingCallbacks = newTimingCallbacksImpl
33+newTimingCallbacks = runEffectFn2 newTimingCallbacksImpl
3434
35−foreign import newTimingCallbacksImpl :: TuneObject -> TimingOptions -> Effect TimingCallbacks
35+foreign import newTimingCallbacksImpl :: EffectFn2 TuneObject TimingOptions TimingCallbacks
3636
3737start :: TimingCallbacks -> Effect Unit
38−start = startImpl
38+start = runEffectFn1 startImpl
3939
40−foreign import startImpl :: TimingCallbacks -> Effect Unit
40+foreign import startImpl :: EffectFn1 TimingCallbacks Unit
4141
4242stop :: TimingCallbacks -> Effect Unit
43−stop = stopImpl
43+stop = runEffectFn1 stopImpl
4444
45−foreign import stopImpl :: TimingCallbacks -> Effect Unit
45+foreign import stopImpl :: EffectFn1 TimingCallbacks Unit
4646
4747pause :: TimingCallbacks -> Effect Unit
48−pause = pauseImpl
48+pause = runEffectFn1 pauseImpl
4949
50−foreign import pauseImpl :: TimingCallbacks -> Effect Unit
50+foreign import pauseImpl :: EffectFn1 TimingCallbacks Unit
5151
5252reset :: TimingCallbacks -> Effect Unit
53−reset = resetImpl
53+reset = runEffectFn1 resetImpl
5454
55−foreign import resetImpl :: TimingCallbacks -> Effect Unit
55+foreign import resetImpl :: EffectFn1 TimingCallbacks Unit
5656
5757-- | Jump the cursor to a fraction of the tune (0.0 to 1.0).
5858setProgress :: Number -> TimingCallbacks -> Effect Unit
59−setProgress = setProgressImpl
59+setProgress = runEffectFn2 setProgressImpl
6060
61−foreign import setProgressImpl :: Number -> TimingCallbacks -> Effect Unit
61+foreign import setProgressImpl :: EffectFn2 Number TimingCallbacks Unit
src/Abcjs/Types.purs+16−1
@@ -1,5 +1,7 @@
11module Abcjs.Types
2− ( TuneObject
2+ ( Selector(..)
3+ , Abc(..)
4+ , TuneObject
35 , tuneLineCount
46 , TimingEvent
57 , eventMilliseconds
@@ -10,6 +12,19 @@ module Abcjs.Types
1012 , eventHeight
1113 ) where
1214
15+import Data.Newtype (class Newtype)
16+
17+-- | A CSS selector (or element id) naming where to draw, distinct from the
18+-- | music itself so the two can never be passed in the wrong order.
19+newtype Selector = Selector String
20+
21+derive instance Newtype Selector _
22+
23+-- | ABC notation source text — the thing you generate for the game.
24+newtype Abc = Abc String
25+
26+derive instance Newtype Abc _
27+
1328-- | A parsed/rendered tune, as returned by `renderAbc` or `parseOnly`.
1429-- | Opaque: pass it back into the synth and timing APIs.
1530foreign import data TuneObject :: Type
test/Test/Main.purs+4−4
@@ -3,7 +3,7 @@ module Test.Main where
33import Prelude
44
55import Abcjs.Render (parseOnly)
6−import Abcjs.Types (tuneLineCount)
6+import Abcjs.Types (Abc(..), tuneLineCount)
77import Data.Array (head, length)
88import Data.Maybe (Maybe(..))
99import Effect (Effect)
@@ -22,12 +22,12 @@ main = runSpecAndExitProcess [ consoleReporter ] do
2222 Just tune -> tuneLineCount tune `shouldSatisfy` (_ > 0)
2323 Nothing -> fail "expected a tune object"
2424 it "parses empty input to a tune with no music" do
25− case head (parseOnly "") of
25+ case head (parseOnly (Abc "")) of
2626 Just tune -> tuneLineCount tune `shouldEqual` 0
2727 Nothing -> fail "expected a tune object even for empty input"
2828
29−validTune :: String
30−validTune = twoMeasures "C D E F" "G A B c"
29+validTune :: Abc
30+validTune = Abc (twoMeasures "C D E F" "G A B c")
3131
3232twoMeasures :: String -> String -> String
3333twoMeasures a b = header <> a <> "|" <> b <> "|"