<- Back to garden entrance

so, purescript has a pretty cool extensible effects system

well it's not built into the language, but there's a library that uses row types to make it

basically you describe an API by making a functor, something like this:

data InputAPI a
  = TextInput String (String -> a)
  | SelectOptions Opts (OptID -> a) -- imagine Opts is a type that represents a list of selectable options, and OptID identifies an option
  | SearchOptions SearchOpts (OptID -> a) -- similar story here

derive instance Functor InputAPI

then some boilerplate for lifting it into the Run monad:

type INPUT r = (input :: InputAPI | r) -- this is a type row containing the labeled type InputAPI, as well as whatever types are in the type row `r`. this is what lets us make it extensible

textInput :: forall r. String -> Run (INPUT + r) String -- given a string (user prompt), gives a `Run`-action that returns a string (user response)
textInput name = lift @"input" $ TextInput name identity

selectOptions :: forall r. Opts -> Run (INPUT + r) OptID -- given a list of options, gives a run action that returns the id of the selected option
selectOptions opts = lift @"input" $ SelectOptions opts identity -- we could have put something other than identity here to not directly return the option id, but maybe do a lookup in the option map so that the caller doesn't need to do that themself

-- likewise for searchOptions

now you can use this API to make little "programs":

-- yes, this is for my pokemon card collection tracking website
registerTrade :: forall r. Run (INPUT + r) Trade
registerTrade = do
  tradeWith <- textInput "Trade with"
  receivedCard <- searchOptions allCards
  givenCard <- searchOptions ownedCards
  pure $ mkTrade tradeWith receivedCard givenCard -- just constructs a `Trade` data type for this example

now, this doesn't actually do anything yet, it's just a static representation of the actions to take

but now we can make different interpreters. for example a console frontend for this input api:

consoleImpl :: InputAPI ~> Aff
consoleImpl (TextInput label result) = do
  Console.log label
  result <$> getUserInput -- here i'm just assuming that we have some functions that do what we want in the console
consoleImpl (SelectOptions opts result) = do
  picked <- showSelectMenu opts
  pure (result picked)
consoleImpl (SearchOptions opts result) = do
  picked <- showSearchMenu opts
  pure (result picked)

runAsConsole :: forall r. Run (INPUT + AFF + r) ~> Run (AFF + r) -- the INPUT field disappears here, meaning it's been handled, and the abstract API "calls" have been transformed to AFF (async effect) actions.
runAsConsole = someFunctionIForgetTheNameOf consoleImpl

...or a browser-based interface which uses html elements to get user input:

webImpl :: InputAPI ~> Aff
webImpl (TextInput label result) = do
  -- maybe create a text field element and a "submit" button or something like that, and setup an event listener to wait for the user pressing the submit button
webImpl (SelectOptions opts result) = do
  -- here maybe create an element list with events where if you click one it resolves with a value
webImpl (SearchOptions opts result) = do
  -- similar story here probably

runAsWeb :: forall r. Run (INPUT + AFF + r) ~> Run (AFF + r) -- web impl also gets run as AFF
runAsWeb = someFunctionIForgetTheNameOf webImpl

and now we have just one registerTrade function, but we can do runAsWeb or runAsConsole depening on our platform, using the same code as backend for two different input methods

pretty cool!

there are some issues with this

firstly, the Run api is monadic, meaning that each action has the ability to depend on a value from previous actions, meaning that they can't run in parallel. so in this example, it would first create an element with the input and the submit button, and then when you press submit, it would be able to move on to the next one and create the search menu. which is a bit annoying

this can be solved by using Applicative instead of Monad, which requires using something other than Run that is applicative instead of monadic. not aware of anything like this that exists so i would have to make it myself i guess, but i have dabbled in this area before and might even have something working laying somewhere in my coding folder

but, the thing with applicatives is that they can't depend on previous actions. everything sort of happens in parallel (but not necessarily in practice). that lets each action do its thing and wait for the user to respond, and have the whole registerTrade resolve when everything has been inputted. you could possibly maybe even have a shared submit/confirm button that the user can press when they've inputted all the fields in the web interface

in the console interface you'd just keep it sequential

the other problem is that this means creating HTML elements on the fly or maybe on page load. i don't like doing that, at least not unless i'm making an SPA, and i don't want to make an SPA

this is where the really cool part about applicatives comes in

since actions exist by themselves in a way, and can't depend on the results of other actions, we don't actually need to have a result at all

these actions actually want us to like, return the string the user inputted, or the id of the option the user selected, but if we're interpreting it into an applicative, we don't need to

so we can additionally make an interface like this:

genElementsImpl :: forall a. InputAPI a -> Const HTML a -- Const is a data type constructor that takes two types. the first is what it actually contains, and then it takes a second one that's just there, without being contained in the type at all. So `Const HTML a` just has HTML in it, and the `a` that InputAPI was expected to return just isn't there
genElementsImpl (TextInput label _) = -- don't need the result function here, we don't even have a result to apply it to
  pElement label <> inputElement -- made-up API for making html elems
genElementsImpl (SelectOptions opts _) = elemList otps -- or something
genElementsImpl (SearchOptions opts _) = searchInterfaceFrom opts
-- these elements should probably also have some consistent ID so we know how to refer to them later

genElements :: forall a. ApplicativeRun (INPUT + ()) a -> HTML -- here we just say that the `Run` can't contain any extra effects, cause we don't have a way to handle them
genElements = -- here we need to go through the Run structure, calling `genElementsImpl` on every InputAPI that's in there, and collecting up all the HTML. maybe this is where we add a `submit` button or something.

then, at build time (or i guess it's sorta like a datagen step), we can run our registerTrade with this interpreter to generate the HTML we need and put it into some larger document that gets statically served

and then the runAsWeb interpreter can know about the same IDs for the elements, so its job becomes listening for the submit button press and reading the values from the various input sources on the webpage

then we have the business logic of the user interface in one place, referencing an abstract input api. and we have three different "interpreters" for it:

anyone interested can read more about Run here, there's a nice little intro to it as a readme if you just scroll down