Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Suggestions for Reader integration on top of the effects api #15

Open
mlegenhausen opened this issue Jul 4, 2018 · 5 comments
Open

Comments

@mlegenhausen
Copy link

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?

@gcanti
Copy link
Owner

gcanti commented Jul 4, 2018

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

@mlegenhausen
Copy link
Author

@gcanti thanks for the feedback will try it out

@mlegenhausen
Copy link
Author

mlegenhausen commented Jul 4, 2018

@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?

@gcanti
Copy link
Owner

gcanti commented Jul 5, 2018

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>>
}

@mlegenhausen
Copy link
Author

@gcanti thanks again for your help.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants