I clearly see the cyclic dependency in your example but I don’t yet have a clear understanding of why it is inevitable. I also feel uncertain if the example truly implies that cyclic dependencies are unavoidable when trying to create an easier to use version of elm-program-test.
I have a partial implementation of the interface in a current project which compiles but maybe some part of the interface can’t be added without introducing a cycle. An explanation of the current approach I’m experimenting with might help. It consists of three modules Real, Simulated, and Command. These modules consist of mostly just wrappers and aliases. Real wraps the elm Cmd stuff. Simulated wraps the SimulatedEffect stuff in elm-program-test. Command provides the means to switch between the two by changing a single import. All other modules in the project refer only to Command. Minimal examples of the three modules follow.
module Simulated exposing
( Body
, Command
, Expect
, batch
, httpGet
, none
)
import ProgramTest
import SimulatedEffect.Cmd
import SimulatedEffect.Http as Http
type alias Command msg =
ProgramTest.SimulatedEffect msg
type alias Body =
Http.Body
type alias Expect msg =
Http.Expect msg
none : Command msg
none =
SimulatedEffect.Cmd.none
batch : List (Command msg) -> Command msg
batch =
SimulatedEffect.Cmd.batch
httpGet : { url : String, expect : Http.Expect msg } -> Command msg
httpGet =
Http.get
module Real exposing
( Body
, Command
, Expect
, batch
, httpGet
, none
)
import Http
type alias Command msg =
Cmd msg
type alias Body =
Http.Body
type alias Expect msg =
Http.Expect msg
none : Command msg
none =
Cmd.none
batch : List (Command msg) -> Command msg
batch =
Cmd.batch
httpGet : { url : String, expect : Http.Expect msg } -> Command msg
httpGet =
Http.get
module Command exposing
( Body
, Command
, Expect
, batch
, httpGet
, none
)
{- Toggle these imports to switch between production and testing. -}
import Real as Base
-- import Simulated as Base
type alias Body =
Base.Body
type alias Command msg =
Base.Command msg
type alias Expect msg =
Base.Expect msg
none : Command msg
none =
Base.none
batch : List (Command msg) -> Command msg
batch =
Base.batch
httpGet : { url : String, expect : Base.Expect msg } -> Command msg
httpGet =
Base.httpGet
So, it isn’t a drop in replacement in the sense that I can just add a line like import Command as Cmd and be done. However, I might call it a one-to-one replacement in that I just have to make some text substitutions like replace Http.get with Command.httpGet. The compiler and type system, as always, help locating missed changes. If I’m starting a new project neither method seems to add any significant overhead.
The Real and Simulated modules could be packaged up or incorporated into elm-program-test so that they are installable like any other package. It appears the Command module is something you have to basically cut and paste into your code. We need to either make a substitution in this file to switch between Real and Simulated or do something like keep separate versions of it in the production and testing branches of our repo.
You point out that to do something like this:
…you need to do it in all dependencies too
I agree. I remember the frustration of implementing just the handful of functions I needed and uncovering dependency after dependency that had to be met first. But, I eventually got to the bottom of it. If others think this is a worthwhile approach then it can be done once and shared so everyone doesn’t have to go through the same frustration. It’s mostly just a lot of boilerplate that the compiler guides you through. @avh4 has already done the tough work.
I agree it isn’t an ideal solution but it seems preferable in some if not most cases compared to the method currently suggested in the elm-program-test example. Both versions require you to replace your Cmd types with a different type. In the example version you have to write your own type that includes logic to bundle up your parameters. In the version I propose you only need to copy a file into your project. It also comes with the small benefit that it gets compiled away in production.
I don’t know if either proposed solution qualifies as good but I think they both might meet the standard of better and I’m curious to hear what others think or discussing ways to improve these approaches.