Creating an empty opaque parameterized type


#1

I’m trying to create a simple Schedule type that will allow storing Arrays of arbitrary types, and retrieve them as they become ‘due’. I want the main type to be opaque to avoid exposing the implementation to code that uses it. However I am having trouble creating an ‘empty’ schedule.

If I don’t make the type opaque, then this compiles fine:

import Array exposing (Array)
import Dict exposing (Dict)

type alias Schedule x =
    { today : Int
    , maxItemsPerDay : Int
    , schedule : Dict Int (Array x)
    }

empty : Schedule x
empty =
    { today=0, maxItemsPerDay=1, schedule=Dict.empty}

But if I try and make the type opaque like so…

import Array exposing (Array)
import Dict exposing (Dict)

type Schedule x =
    Schedule x
        { today : Int
        , maxItemsPerDay : Int
        , schedule : Dict Int (Array x)
        }

empty : Schedule x
empty =
    Schedule x { today=0, maxItemsPerDay=1, schedule=Dict.empty}

then the last line fails to compile with the error Cannot find variable 'x'

Trying to be more specific like this:

empty : Schedule x
empty =
    let
        start : Dict Int (Array x)
        start =
            Dict.empty
    in
        Schedule x { today=0, maxItemsPerDay=1, schedule=start}

doesn’t help.

What do I need to do?


#2

this should do the job…

type Schedule x =
    Schedule
        { today : Int
        , maxItemsPerDay : Int
        , schedule : Dict Int (Array x)
        }

empty : Schedule x
empty =
    Schedule { today=0, maxItemsPerDay=1, schedule=Dict.empty}

(sorry - have to dash so no explanation)


#3

To avoid substituting dummy values (0 or 1 for Int), it would be safer to do:

type Schedule x =
    Schedule
        { today : Int
        , maxItemsPerDay : Int
        , schedule : Dict Int (Array x)
        }
   | EmptySchedule

Then you only have so supply legitimate Int values to non-empty schedules.

This is similar to using Maybe Int in a situation where you may have an int value or no value at all. In languages without tagged unions, you might use null (Java) or -1 ( C), as a stand-in for ‘no-value’.


#4

Thanks. When you (or someone else) has more time could you provide an explanation? My guess is that it’s something to do with the Constructor not taking parameters even though the type does, but it would be good to get it a bit clearer in my head!


#5

Thanks rupert. I actually simplified things a bit. The empty function actually takes an Int so that you have to specify the maxItemsPerDay up front on creation. The today field is just a counter that gets advanced and is used to work out where in the dict to store something (as when adding to the schedule you will always specify “n days from today”) so it’s initial value for an empty schedule is always arbitrary.

The idea is you first create an empty Schedule up front saying what the maximum number of items that can be scheduled on a day is and then build it up by scheduling things into that empty Schedule.

But yes, in the simplified way I presented the empty function a union type would have been sufficient.

Thanks!


#6

O.K., here is an explanation:

When you define the data type:

type Schedule x =
    Schedule x
        { today : Int
        , maxItemsPerDay : Int
        , schedule : Dict Int (Array x)
        }

What exactly is defined here?
To look at it more closely, you can just write the name of the type in the elm REPL, and it will tell you what the type is:

> Schedule
<function>
    : x  -> { maxItemsPerDay : Int
         , schedule : Dict.Dict Int (Array.Array x)
         , today : Int
         }
      -> Repl.Schedule x

If we were to hide the body of the record for a short while to make it more apparent what is going on, it looks like this:

> Schedule
<function> : x -> {...stuff...} -> Repl.Schedule x

Woah! So Schedule is a function that takes an element of type x and a record of the proper format, and returns a Schedule x.

So, Schedule takes two arguments, the first one being the thing of type x you want to store. And this is why the definition of empty that you had creates a problem: The constructor to create a new element of type Schedule is not Schedule x but rather Schedule.

Elm sees you attempting to pass an x as first argument to this constructor function, but note that we are not describing a type right now, but are in data land: There is no data variable (a ‘normal’ variable) called x in this scope. You have used a type variable that happens to have the name x in the function type signature, but the variable names you use in type-land and the names you use in data-land are completely unconnected. (Only in dependent languages like Idris might this sometimes be different).

And this is the case that the compiler complains that x is not known in the definition of empty.

Actually, there is no way to make empty compile successfully for the type of Schedule as you wrote it (other than using Debug.crash which is not useful), because we cannot pluck an element of x out of thin air: So either the constructor needs to change, letting us omit the x, or you need to ask the user to pass in another variable. (Naming the variables in data land something else than those in type land can of course help to reduce confusion as well).

What @roovo thus wrote is probably (since I do not think that you wanted to add an x to every Schedule) what you want.

So in summary:

  • your confusion probably stems from the fact that the data constructor function is actually called Schedule and not Schedule x.
  • Even if things in type signatures and in function implementations might have the same name, they mean something different, and you cannot refer to a type variable from an implementation or the other way around, they are separate worlds.

#7

Thanks! I think I understand it now.

While I knew in theory that the constructor is a function, I was still subconsciously thinking of it more as a label saying “what follows should be coerced to this type” and as a result couldn’t understand why one constructor could create Schedule’s of different types.

Really thinking carefully about it as a function that just takes a “record using some type x” and returns a Schedule of type “Schedule x” makes it clear why there only needs to be the one constructor for all possible x’s.

Thanks everyone for your help :slight_smile:


#8

PS If I could choose more than one solution I’d mark your reply as one too @roovo. I chose the one with additional explanation just to help any others that might find this post!


#9

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