Elm help

I’ve asked on the Elm mailing list but have had no response, and as many here seem to be interfacing Elm and Phoenix as well then I am hoping someone might have some help/ideas. :slight_smile:

My Elm-Mailing-List Post:

Two questions about ports.

  1. Why is Dict not supported? I have a dictionary of mixed string/integers keys and a concrete record-stable from JSON passed in via a port, everything else decomposes fine into a type except for it.

  2. And this is my big question that I am having trouble working around without great verbosity. I have these types:

type alias PresenceMeta metaUserType =
    { metaUserType
        | phx_ref : String
    }

type alias RoomSyncMetas =
    { loc : String
    , online_at : String
    }

type alias Testeringa =
    PresenceMeta RoomSyncMetas

And a port like:

port testering : (Testeringa -> msg) -> Sub msg

And it generates this error:

Port `testering` is trying to communicate an unsupported type.

173| port testering : (Testeringa -> msg) -> Sub msg
     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The specific unsupported type is the following extended record:

    { phx_ref : String, loc : String, online_at : String }

The types of values that can flow through in and out of Elm include:

    Ints, Floats, Bools, Strings, Maybes, Lists, Arrays, Tuples, Json.Values,
    and concrete records.

So it says that { phx_ref : String, loc : String, online_at : String } is not a supported type, which seems… odd. So I change the Testeringa type to:

type alias Testeringa =
    { phx_ref : String
    , loc : String
    , online_at : String
    }

And no other changes, and then it compiles fine.

My initial reaction to this is wtf…? What did I break?!

2 Likes

Disclosure: I barely glossed over the tutorial of Elm - but I’m willing to play “rubber duck” - especially since Elm has a reputation for helpful error messages - i.e. take a close look:

The specific unsupported type is the following extended record

and

The types of values that can flow through in and out of Elm include: … and concrete records.

Basically your last change turned an extended record into a concrete record. So my guess is that the previous error message is trying to convey that extended (or extensible) records aren’t supported on ports.

Also Dicts through ports? may be of interest.

1 Like

I thought of that but it dumps and renders as a concrete record since I am returning it?

1 Like

My interpretation is that ports can’t deal with extensible records - period.

Comments like

You can also define extensible records. This use has not come up much in practice so far, but it is pretty cool nonetheless.

don’t exactly inspire me with confidence that extensible records are a fully fleshed out feature.

The list of supported types should more specifically state “concrete records” (just like the error message). For anything more fancy than the listed basic types a custom JSON Decoder/Encoder seems to be necessary.

1 Like

Just an FYI - a recent comment by Evan Czaplicki:

I think Tessa put it very nicely in her post: extensible records are for making functions flexible, not for modeling your data.

He is referring to “Writing friendly Elm” code by Tessa Kelly at NoRedInk.

So it seems after the removal of record field addition and deletion in 0.16 and dropping support for extensible and extended record constructors the use of the extensible record syntax may only be recommended for function type signatures rather than for type definition; e.g.:

getName : { b | name : a } -> a
getName { name } = name
1 Like

That is what I use it for, but to redirect through a port, there is no data modeling there but rather it is just to make an easy custom interface for sending information to/from ports in my port library.

1 Like

This reflects my current understanding of the situation (0.17)

-- This defines an "extensible record" type
-- useful for parameter type specification
-- within function type annotations where 
-- the function only needs access to phx_ref
-- but doesn't care about anything else 
type alias PresenceMeta metaUserType =
    { metaUserType
        | phx_ref : String
    }

-- This is a "concrete record" type
type alias RoomSyncMetas =
    { loc : String
    , online_at : String
    }

-- This is an "extended record" type
-- i.e. the type combines an "extensible record" 
-- with another record, in this case a "concrete record"
-- This is the use case that "Writing friendly Elm" 
-- actively discourages as the "extensible record" is used 
-- for *modeling data*. This is also the type that the port 
-- has a problem with.
type alias Testeringa =
    PresenceMeta RoomSyncMetas

-- This is the alternate version of the last record type
-- this time modeled entirely as a "concrete record"
-- which the port accepts
type alias TesteringaC =
    { phx_ref : String
    , loc : String
    , online_at : String
    }

Note that constructors for extensible records were introduced in 0.9 but dropped in 0.16. For the time being Evan Czaplicki favors record nesting over record extension - though there are dissenting opinions.

So the capabilities of “extensible records” have been scaled back since 0.16 and, for the time being, their endorsed use is for type annotations rather than record type composition.

Hmm, well this is my whole elm-side API right now:

port module JSPhoenix exposing (..)

import Json.Encode
import Json.Decode
import Date exposing (..)
import Date exposing (Date)
import Date.Extra as Date
import Date.Extra.Facts exposing (monthFromMonthNumber)


-- Phoenix channel message receiver handling


type alias CallbackPortMsg =
    { portName : String
    , msgID : String
    , cb_data : Json.Encode.Value
    }


type alias CallbackPortEvent =
    { portName : String
    , cb_data : Json.Encode.Value
    }


type alias ChanExitCB msg =
    { topic : String
    , msg : msg
    }



-- Connecting


type alias Connect =
    { topic : String
    , timeout_ms :
        Maybe Int
        --  Just 10000 -- Default value
    , chanCloseCB : Maybe CallbackPortEvent
    , chanErrorCB : Maybe CallbackPortEvent
    , syncState : Maybe CallbackPortEvent
    , syncJoin : Maybe CallbackPortEvent
    , syncLeave : Maybe CallbackPortEvent
    , joinData : Json.Encode.Value
    , joinEvents :
        List CallbackPortMsg
        -- Valid msgID's are:  "ok", "error", "timeout"
    , onPorts : List CallbackPortMsg
    }


port jsphoenix_connect : Connect -> Cmd msg


connect : Connect -> Cmd msg
connect connect_struct =
    jsphoenix_connect connect_struct



-- Disconnecting


type alias Disconnect =
    { topic : String
    , chanLeavingCB : Maybe String
    }


port jsphoenix_disconnect : Disconnect -> Cmd msg


disconnect : Disconnect -> Cmd msg
disconnect disconnect_struct =
    jsphoenix_disconnect disconnect_struct



-- Pushing


type alias Push =
    { topic : String
    , mid : String
    , msg : Json.Encode.Value
    , pushEvents :
        List CallbackPortMsg
        -- Valid msgID's are:  "ok", "error", "timeout"
    }


port jsphoenix_push : Push -> Cmd msg


push : Push -> Cmd msg
push push_struct =
    jsphoenix_push push_struct



-- Receiving


type alias ChannelEventMsg msg_type cb_type =
    { topic : String
    , msgID : String
    , msg : msg_type
    , cb_data : cb_type
    }


type alias ChannelGenericEventMsg =
    ChannelEventMsg Json.Decode.Value Json.Decode.Value



-- Phoenix.Presence


type alias PresenceMeta metaUserType =
    { metaUserType
        | phx_ref : String
    }


type alias PresenceMetas metaUserType =
    List (PresenceMeta metaUserType)


type alias PresenceObject msgUserType metaUserType =
    { msgUserType
        | metas : PresenceMetas metaUserType
    }


type alias PresenceObjects msgUserType metaUserType =
    List ( String, PresenceObject msgUserType metaUserType )


type alias PresenceDiff msgUserType metaUserType =
    { leaves : PresenceObject msgUserType metaUserType
    , joins : PresenceObject msgUserType metaUserType
    }


type alias PresenceDiffChange msgUserType metaUserType =
    { id : String
    , old : PresenceObject msgUserType metaUserType
    , new : PresenceObject msgUserType metaUserType
    }



-- Various Integration helpers


type alias TimexDateTime =
    { year : Int
    , timezone :
        { until : String
        , offset_utc : Int
        , offset_std : Int
        , full_name : String
        , from : String
        , abbreviation : String
        }
    , second : Int
    , month : Int
    , minute : Int
    , millisecond : Int
    , hour : Int
    , day : Int
    , calendar : String
    }


convertTimexDateToElmDate : TimexDateTime -> Date
convertTimexDateToElmDate { year, month, day, hour, minute, second, millisecond, timezone } =
    Date.fromSpec
        (Date.offset <| timezone.offset_utc + timezone.offset_std)
        (Date.atTime hour minute second millisecond)
        (Date.calendarDate year (monthFromMonthNumber (month + 1)) day)

I only really needed extensible records in the Phoenix.Presence part because the user needs to be able to define the port messages that it will be able to accept in the metas and parent messages both (I like to cause errors at the port level if I set something wrong so I know about it quickly instead of getting just a Json.Encode.Value and potentially and accidentally accepting other things)…

Given your familiarity with your solution this may come across as a silly question: but does moving to a “nested” (rather than “extended”) variant like:

type alias PresenceMeta metaUserType =
    { phx_ref : String
    , meta : metaUserType
    }

type alias PresenceMetas metaUserType =
    List (PresenceMeta metaUserType)

type alias PresenceObject msgUserType metaUserType =
    { msg : msgUserType
    , metas : PresenceMetas metaUserType
    }

type alias PresenceObjects msgUserType metaUserType =
    List ( String, PresenceObject msgUserType metaUserType )

type alias PresenceDiff msgUserType metaUserType =
    { leaves : PresenceObject msgUserType metaUserType
    , joins : PresenceObject msgUserType metaUserType
    }

type alias PresenceDiffChange msgUserType metaUserType =
    { id : String
    , old : PresenceObject msgUserType metaUserType
    , new : PresenceObject msgUserType metaUserType
    }

expose similar or new pain points - i.e.

  • still won’t work or is a pain to work with through the ports?
  • the nested maps are a pain on the phoenix/elixir side?

This is a direct injection from Phoenix.Presence. The reason I do not use extended records on normal messages is that I control those, but I do not control Phoenix.Presence records so it has to remain in the format that it is (I am minimizing JS code for upkeep reasons).