Exposing Alias Types, i find it very irritating

Lets start with an example:

import MyModule exposing (TypeA,TypeB,TypeC(..))

One of these types is a type with exposed constructor, one is an opaque type and one is an alias type.
As you can see the type with exposed constructor is easy to identify, but the opaque type and the alias type are not.

The problem is now that my alias type has a constructor while my opaque type has not. In a bigger project this can get quite annoying.

Let me show you what i mean:
In a package of mine i exedentally used an alias as an opaque type. Even worse: I could chance the definition of the alias type and it was not considered a major chance.
Here is the commit:

Yeah, I know. The commit is a mess.
The change happend in the module Graphics.Abstract, that is used to hide all my opaque types as well as alias types. I learned my lession, now i only have opaque types in the file. Also note: this is 0.18 code!

module PixelEngine.Graphics.Abstract exposing (..)

type alias ContentElement msg =
    {- { elementSource : ElementSource <- this is the old code -}
    { elementType : ElementType
    , customAttributes : List (Attribute msg)
    , uniqueId : Maybe String
    }

There was actually a test file using the ContentElement constructor that broke as a result of this change. And up untill now I should be the only one using this package, so no real code was hurt by this change. But still, i had expacted it to be a major change.

Im not saying that i blame the versioning-system, im just saying that it is very confusing.

So how could we fix this?
we could force alias types to be epxosed with (…). This way at least we know that an element has a useable constructor. Personally i do not like this chance, also because it would not be able to be implemented in 0.19.

i propose to add a new syntax:

import MyModule exposing ((TypeA),TypeB,TypeC(..))

This syntax could be optional. And we could let elm-format help us replace the existing code. When using it, the difference between opaque and alias type would be always clear. This proposal would not break any existing code and could be implemented in 0.19.

If you have another idea how one could make that difference clearer, please, now is your chance. :wink:

The change happend in the module Graphics.Abstract, that is used to hide all my opaque types as well as alias types.

I think part of your frustration here may be caused by a misunderstanding of type aliases. When you define a type alias, you aren’t creating a new type. Instead, you’re creating an alternate name for an existing type. This means you can’t really hide a type alias.

If the underlying type you’re renaming is available outside the module, anyone can construct it or use it in a type signature. They can even give it their own alternate name. All of which are compatible and will type-check with your “hidden” alias. Hiding type aliases generally provides no type safety benefits

Of course, if you alias an opaque type then the alias is also effectively opaque. Remember, an alias is just an alternate name for an existing type.

4 Likes

Here’s a concrete example. If you try to define a type alias in a module and then don’t export it.

module User exposing (default)

type alias User = { name : String, age : Int }

bob : User
bob =
  User "Bob" 42 

In a different module, I can still construct valid users as well as use the underlying type in signatures.

module OtherModule exposing (..)

alice : { name : String, age : Int }
alice =
  { name = "Alice", age : 39 }

ageDifference : { name : String, age : Int } -> { name : String, age : Int } -> Int
ageDifference user1 user2 =
  abs (user1.age - user2.age)

Even though neither User type alias nor the constructor have been exposed, I can still execute code like

OtherModule.ageDifference User.bob OtherModule.alice
-- 3
1 Like

That’s true but it also relies on the fact that all record types already exist in the universe and hence we are just giving a name to something that already exists.

Many uses of records, however, don’t feel that way. Creating a product type feels just like creating a sum type except that you have to stick the word alias in. This has been confusing to every programmer I’ve watched trying to learn Elm which is a small sample size but the results have been 100% on it being a stumbling block.

So, arguably, what would be useful is not so much changing how type aliases are exported but rather providing a way to create new record types without having to wrap them in custom types which then have to be stripped off and reapplied whenever working with the records. (At least 0.19 now eliminates the memory overhead of wrapping them this way.) The export syntax could even control which fields were publicly revealed if getting ambitious with this.

Mark

(*) That’s not to say that having something that behaved like an alias inside a module and an opaque type outside a module wouldn’t be useful. It’s just that I suspect records are the most frequent pain point.

1 Like

Okey. So what im getting here is, that im currently doing something wrong.
Let me try to summarize what i understood so far:

  1. Alias should be used to increase readability, not to introduce new “types”.But then again, i feel like the model-type is the biggest exception to that rule.
  2. if im planning to expose a record, i should always wrap it in a custom type. Is there any exception, for when an exposed alias would be recommanded?

Yeah… Thats the main reason why i typically do not like to use wrapped records.

Model isn’t an exception. If you have for example type alias Model = { a: Int }, then you can use { a: Int } instead of Model anywhere in your program. Of course with large Model that isn’t practical, which is exactly why type aliases are useful.

oh no, thats not what i meant!

i wanted to say that if one wants define a type ( := a new thing that will be used a lot) one should not use an alias, but wrap the record in a custom type:

type Model = Model {a: Int }

but i feel like this would be unnecessary for the model type, as it so much, that a custom type would be really tedious

It is impossible to create a new type using an alias because that’s not what aliases do. The only thing aliases do is allow you to provide an alternate name for an existing type.

Only type can create new types, never type alias. As mentioned by @MarkHamburg the distinction between the two is often a source of confusion for newcomers to Elm.

I often expose type aliases, especially for records that are used in other places outside the current module. I haven’t found that wrapping a custom type around a record to be a useful default thing to do. The bare record type (or an alias to it) is usually perfectly fine to use :slightly_smiling_face:

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