Elm Compiler Too Smart? Appears to delete function calls

What may be going is that the Elm compiler recognizes we don’t use a return value, so it completely eliminates even calling this function.

Here’s an example:

port module Audio exposing (..)

import Browser
import Html exposing (Html , audio , button , div , h1 , text)
import Html.Attributes exposing (controls, id, src, style)
import Html.Events exposing (onClick)
import Task exposing (succeed)

exampleFile = "fsf.ogg"

run : msg -> Cmd msg
run m =
    Task.perform (always m) (Task.succeed ())

-- MAIN
main : Program () Model Msg
main =
    Browser.element
        { init = \_ -> init
        , view = view
        , update = update
        , subscriptions = \_ -> Sub.none
        }

-- PORTS
port playMusic : () -> Cmd msg
port setSource : String -> Cmd msg

-- MODEL


type alias Model =
    { song : String
    }


init : ( Model, Cmd Msg )
init =
    ( { song = "" }
    , Cmd.none
    )

-- UPDATE
type Msg
    = Play (Maybe String)
    | Set String


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        Play mf ->
            case mf of
                Nothing ->
                    ( model
                    , playMusic ()
                    )

                Just f ->
                    {-- Attempt 0
                    let
                        _ = setSource exampleFile
                    in
                    ( model
                    , playMusic ()
                    )
                    --}

                    {-- Attempt 1
                    let
                        (m, _ ) = update (Set f) model
                    in
                    ( model
                    , playMusic ()
                    )
                    --}
                    run (Set f)
                    |>
                        (\r ->
                            ( model 
                            , playMusic ()
                            )
                        )



        Set message ->
            ( { model | song = message }
            , setSource message
            )



-- VIEW


view : Model -> Html Msg
view model =
    div []
        [ h1 [ id "song-header" ] [ text ("Current song: " ++ model.song) ]
        , div [ Html.Attributes.class "song-holder" ]
            [ mkButton (Set "song1.mp3") "Song 1"
            , mkButton (Set exampleFile) "Song 2"
            , mkButton (Set "song3.mp3") "Song 3"
            ]
        , div [ Html.Attributes.class "audio-holder" ]
            [ mkButton (Play Nothing) "Play"
            , mkButton (Play (Just exampleFile)) "Play Example File"
            ]
    , audio [ src "./song3.mp3", id "audio-player", controls True ] []
    ]

mkButton : Msg -> String -> Html Msg
mkButton msg text =
    button
        [ onClick msg
        , style "padding" "0.5rem"
        , style "margin" "0.3rem"
        , style "background" "grey"
        ]
        [ Html.text text ]
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8" />

  <title>Elm</title>
  <script src="audio.js"></script>
  <link href="index.css" rel="stylesheet">
</head>

<body>
  <div id="app"></div>
  <script>
var app = Elm.Audio.init
(
    { node: document.getElementById('app')
    }
);

document.getElementById("audio-player").volume = 0.2;

app.ports.playMusic.subscribe(function() {
    document.getElementById("audio-player").play();
});

app.ports.setSource.subscribe(function(source) {
	console.log(`Setting source to ${source}`);
    document.getElementById("audio-player").setAttribute("src", source);
});

  </script>
</body>
</html>


Basically Play (Just "Somefile.mp3") should call setSource first, but Elm never calls this function.
Calling setSource file on its own does work as expected.

Did anyone run into this issue? If so how did you solve it?

1 Like

This should result in nothing happening because the result of setSource is never used. In Elm, a Cmd is something you pass to the Elm runtime and the runtime does something appropriate with it. E.g. Http.get tells the runtime to fetch some data from a url, but doing _ = Http.get { url = "", expect = ... } doesn’t actually do the fetching of the data. Same for you code, you need to pass the return value of setSource exampleFile to the Elm runtime. You can do this by doing

( model
, setSource exampleFile
)

in your update. If you need to send multiple Cmds, you can use Cmd.batch [ cmd1, cmd3, cmd2 ]. Do take note though that the order here is irrelevant (which is why I put cmd3 in the middle), so doing Cmd.batch [ setSource exampleFile, playMusic () ] could either set the source and then play OR play and then set the source.


Separately, it might be better to also set the source in your view code like you do here:

if you store the source in your model as source = "some value" then when you select a different song you can set your audio source as

[ src model.source,
1 Like

Hi wolfadex,

Thank you for your reply!

That confirms my suspicion as to why the issue occurs.

Previous Reply

But what’s a good way to solve it?
Cmd.batch is not suitable because setSource must occur before play.

We could write more javascript, but that seems to defeat the purpose of Elm.

Actually you already answered it. So you update the model with the src, then call play. Let me try that.
So like

SetAndPlay file ->
    ( { model | source = file }
    , playMusic ()
    )

No, that doesn’t work because playMusic gets called before the HTML src attribute is updated via the model.

Why not just use one port?

You mean like port play : (src : String) -> Cmd Msg ?
And then

app.ports.playMusic.subscribe(function(source) {
    document.getElementById("audio-player").src = source;
    document.getElementById("audio-player").play();
});

That works, thank you.

Now I just wonder how that can be done in Elm.

You likely want to use requestAnimationFrame on the JS side to wait until after Elm has updated the view. This would look like

app.ports.playMusic.subscribe(function() {
  requestAnimationFrame(function() {
    document.getElementById("audio-player").play();
  });
});

Would of guessed something like this works, but no luck:

-- UPDATE
type Msg
    = Play
    | Set String Bool
    | DoneSettingSrc Bool

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        Play ->
            ( model
            , playMusic ()
            )

        DoneSettingSrc play ->
            ( model
            , if play then playMusic () else Cmd.none
            )

        Set f play ->
            let 
                msgDone = Cmd.map (always <| DoneSettingSrc play) Cmd.none
            in
            ( { model | audioSrc = f }
            , msgDone
            )

Was expecting the view to re-render with model.audioSrc = src (meaning audio.src is set), before update (DoneSettingSrc True) is called.

I mean something like

import Browser
import Html exposing (Html, button, div, text)
import Html.Events exposing (onClick)
import Json.Decode
import Json.Encode

type alias Player =
  {  play: Bool
  ,  source: String
  }
  
type alias Model =
    { song : String
    , playing: Bool
    }  

port setPlayer : Json.Encode.Value -> Cmd msg

main =
  Browser.element { 
    init = \() -> ({ song = "firstSong.mp3", playing = False }, Cmd.none)
    , update = update
    , view = view 
    , subscriptions = \model -> Sub.none
  }
  
type Msg = Play | SetSource String | SetSourceAndPlay String

encodedPlayer : Player -> Json.Encode.Value
encodedPlayer player = 
    Json.Encode.object
        [ ( "play", Json.Encode.bool player.play )
        , ( "source", Json.Encode.string player.source )
        ]        

controlPlayer : Player -> Cmd msg
controlPlayer player =
    setPlayer (encodedPlayer player)
    
update msg model =
  case msg of
    Play ->
      ({model | playing = True}, controlPlayer { play = True, source = model.song} )

    SetSource song ->
      ({ model | song = song, playing = False}, controlPlayer { play = False, source = song} )

    SetSourceAndPlay song ->
      ({ model | song = song, playing = True}, controlPlayer { play = True, source = song} )

view model =
  div []
    [ button [ onClick Play ] [ text "Play" ]
    , button [ onClick <| SetSource "secondSong.mp3" ] [ text "Set source" ]
    , button [ onClick <| SetSourceAndPlay "thirdSong.mp3" ] [ text "Set source and play" ]
    , if model.playing then div [] [ text ("Playing " ++ model.song) ] else div [] [ text <| "Click on Play to play " ++ model.song]
    ]

1 Like

Here’s a working example https://ellie-app.com/pn2X2hqfPH4a1

1 Like

To summarize,

wolfadex proposed handling the audio element’s src attribute in elm, then using requestAnimationFrame(func) on the javascript side to ensure the function runs after Elm updated the src attribute.

gorgoroth proposed sending {source: String, play: Bool} out to javascript.

Thank you, both @wolfadex and @gorgoroth, for your approaches!
And of course thank you for explaining why function effects “disappear”.

3 Likes

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