elm-ts
elm-ts copied to clipboard
Suggestions for Reader integration on top of the effects api
I would like to make all dependencies that are involed in side effects to be exchangable. It seems that the most common solution for this problem is the usage of a Reader (my first time using it).
What would be the best way to integrate it in elm-ts? I already experiment a little bit with it and this is what I came up with. I needed to write some code around the elm-ts functions to get it working.
import { empty } from 'rxjs/observable/empty'
import { Observable } from 'rxjs/Observable'
import { Option } from 'fp-ts/lib/Option'
import { perform, Task } from 'elm-ts/lib/Task'
import { Program, Html } from 'elm-ts/lib/Html'
import { program, Location } from 'elm-ts/lib/Navigation'
import { Reader, asks, reader } from 'fp-ts/lib/Reader'
// The new Cmd Signature
export type CmdR<env, msg> = Reader<env, Observable<Task<Option<msg>>>>
export const none: CmdR<any, never> = reader.of(empty())
export type SubR<env, msg> = Reader<env, Observable<msg>>
// Adaption of program to accept CmdR for init, update and subscriptions
export const programWithReader = <env, model, msg, dom>(
locationToMessage: (location: Location) => msg,
init: (location: Location) => [model, CmdR<env, msg>],
update: (msg: msg, model: model) => [model, CmdR<env, msg>],
view: (model: model) => Html<dom, msg>,
subscriptions?: (model: model) => SubR<env, msg>
): Reader<env, Program<model, msg, dom>> => {
return asks((env) => program<model, msg, dom>(
locationToMessage,
location => {
const [model, cmd] = init(location)
return [model, cmd.run(env)]
},
(msg, model) => {
const [newModel, cmd] = update(msg, model)
return [newModel, cmd.run(env)]
},
view,
model => subscriptions ? subscriptions(model).run(env) : empty()
))
}
// Cause our side effect is wrapped in a Reader we need to wrap perform accordently
export const performR = <a, env, msg>(readerTask: Reader<env, Task<a>>, f: (a: a) => msg): CmdR<env, msg> => {
return readerTask.map(task => perform(task, f))
}
I limited the access to the Reader to the commands to prevent the leaking of effectfull dependencies to the rest of the application (view, init and update functions).
I used this code in your elm-ts-todomvc example to make localStorage to be exchangable in unit tests or to make the app server side renderable (where no localStorage is avaibale).
Would this be something reasonable to integrate in elm-ts itself?
the most common solution for this problem is the usage of a Reader
Many people prefer to inject the dependencies explicitly, so another option is using some kind of MTL style. See this PR
@gcanti thanks for the feedback will try it out
@gcanti am I right that it doesn't make sense to apply the full mtl-style from fp-ts to elm-ts? So making it to the more generic interface MonadLocalStorage<M>? Cause I don't know how to transform from any monad M to Task which is used by elm-ts. Is this even possible?
I don't know how to transform from any monad M to Task which is used by elm-ts. Is this even possible?
Theoretically you need a natural transformation, from M to Task, i.e.
toTask: <A>(fa: HKT<M, A>) => Task<A>
That's what fromIO (from the Task module) really is. For example in
const saveToNamespace = (M: MonadLocalStorage) => (value: string): Task<void> => fromIO(M.setItem(NAMESPACE, value))
So a generalization would be something along the lines of
interface MonadLocalStorage<M> {
URI: M
setItem: (key: string, value: string) => HKT<M, void>
getItem: (key: string) => HKT<M, Option<string>>
}
interface MonadTask<M> {
URI: M
toTask: <A>(fa: HKT<M, A>) => Task<A>
}
however in this case it's not worth it as I can't come up with another sensible instance other than IO, thus MonadLocalStorage can just return a concrete type
interface MonadLocalStorage {
setItem: (key: string, value: string) => IO<void>
getItem: (key: string) => IO<Option<string>>
}
@gcanti thanks again for your help.