Init a task in a module + avoid circular import?

Newbie question… There is a value from the model of Main.elm that I would need to access in a module, the module being itself imported in Main…hence the circular import…
is there a way around this, or I just have to rethink my design?
I believe it’s the latter…

At first, I wanted to have this value computed in the module, not in Main, but I needed it to be initialized on the app startup. (This value is computed from the current time, to give you context).
Is there a way to initialize this task in a module, on app startup? I guess so but I couldnt figure it out. Can I just use init without view? Is there something like view.None to tell Elm there is no view here ?

So I ended up doing this in the Main init and it created this circular import bug because I was importing the value from Main into the module…

You can pass arguments (like the current time if you got it as a flag) to the init from another module.

Can you provide more information about this? init is just a regular function, it is not obvious why it would need a view. In any case, an empty view is usually text "".

Reading between the lines, it sounds like you are trying to build your project using a “fractal architecture” with a bunch of modules that each define init/update/view, etc. Most extracted modules in Elm apps are not structured this way. Instead, they are usually just a data structure and some functions that act upon it (think the Elm core library). The init/update/view functions are not magic framework functions and modules don’t need to implement them.

It’s hard to make an definite recommendations without more context on what you are doing but most solutions likely involve passing in the time as an argument as suggested by @pdamoc.

For example you might create some sort of constructor for your custom data structure in the module like:

MyModule.fromTime : Posix -> MyModule.Structure

which you could call from Main like:

module Main exposing(main)

import MyModule

init : Flags -> (Model, Cmd Msg)
init flags =
  (initialModel, Task.perform GotInitialTime Time.now)

update : Msg -> Model -> (Model, Cmd Msg)
update msg model  =
  case msg of
    GotInitialTime time ->
      ({ model | structure = MyModule.fromTime time }, Cmd.none)

Thanks @pdamoc and @joelq for your help! The way I tried to design this is so messy that its hard to explain, huge red flag! :slight_smile: I am still experimenting with Elm and FP.

So maybe its better to explain what I would like to do and maybe you can share with me how you’d design this?

Main.Model holds data decoded from 5 different HTTP requests, that I got it to work well thanks to your advice in this chat:

type Model =
  Loading {
    dataA: Maybe DataA
    ,dataB: Maybe DataB
    , ....etc...

}
| Success {
    dataA: DataA
    ,dataB: DataB
    , .... etc...

}
| Error String

That works beautifully… but of course I don’t want to hold the whole API response or data I dont need in Main.Model.
So, Main.init initiates the API calls, then I import the decoders from the respective modules DataA.elm etc… to parse the responses.
The responses I will get back are nested timeseries data, with hourly timestamps, and for each I am only interested in the value matching the next hour’s timestamp.

...  [
{timestamp: 1231243124, value: 23},
{timestamp: 1231243453, value: 12},
]

So I get this array back, at the decoder level, now how can I “filter in” only the value I am interested in, (the one corresponding to the next hour after I ran the app) ? I just want to update the model with, lets say, 23.

First I will have to first compute the timestamp corresponding to the next hour in my local timezone. I think I am good with that part but where should this timestamp be computed? Main or separate module?
And then, most importantly, how do I share this timestamp with all the 5 decoders?

*Is it possible or a good idea to pass it as an argument to the decoders?
Or should I make a function that takes the time and return a Decoder?
How can I make sure the timestamp will be ready for the decoder?
Init would have to have something like that I assume: getNextHourTimestamp |> andThen Cmd.batch [requests] even if I cant pass it to the actual requests *

I hope it’s more clear… thank you.

First, if you have 5 sets of data it would be better to model each as a remote data. You can take a look remotedata-http for more information. This assumes that you might want to display partial data (data from some of the 5 endpoints as it arrives). If you want an all or nothing approach, you might also look into Http.task and Task.sequence.

Now, on to the filtering. This logic looks to me like it belongs in the backend BUT, if you don’t have access to the backend and there is no way to call it with some timestamp parameter, you can still do the filtering in the decoding step.

import Json.Decode as Json

type alias Timestamp = 
    { timestamp : Int 
    , value : Int 
    }

oneHour : Int 
oneHour = 3600000

timestampDecoder : Decoder Timestamp
timestampDecoder = 
    Json.map2 Timestamp 
        (Json.field "timestamp" Json.int)
        (Json.field "value" Json.int) 

oneHourFilter : Posix -> List Timestamp -> List Timestamp 
oneHourFilter now timestamps = 
    List.filter (\t -> now + oneHour > t.timestamp ) timestamps

timestampListDecoder : Int -> Decoder (List Timestamp) 
timestampListDecoder now = 
    Json.list timestampDecoder 
        |> Json.map (oneHourFilter now)

You would then get the current time and call the endpoints using (timestampListDecoder now)

getTimestamps : Int -> Cmd Msg
getTimestamps now =
  Http.get
    { url = "https://your-host.com/api/some-timestamps"
    , expect = Http.expectJson GotTimestamps (timestampListDecoder now)
    }
1 Like

Thanks a lot, very nice solution, I will try this… (its a public API I don’t have access to the backend).
Maybe the title of my post should be changed to something more in tune with the topic actually, I don’t have the required permission.

Alright so I tried and got stuck here:

getTimestamps: Int -> Cmd Msg

in my example the time value is in the Main Model so it would be:
getTimestamps: Model -> Cmd Msg but its the same thing…

The compiler complains because the initialCmd should be of type Cmd and not Model -> Cmd Msg in main:

main =
 Browser.element
        { init = \_ -> ( initialModel, initialCmd )

I guess it should be:

main =
 Browser.element
        { init = \_ -> ( initialModel, initialCmd initialModel)

It is advisable to give functions as little data as possible so. Keep the functions small and with parameters as simple as possible.

So… instead of getTimestamps: Model -> Cmd Msg where you would extract some data from the mode, prefer getTimestamps: Int -> Cmd Msg.

That is good advice… thank you!

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.