Stream: beginners

Topic: opaque types


view this post on Zulip Georges Boris (Sep 21 2022 at 13:29):

Hey folks - since custom types are defined instantly and they don't need to be imported I take it they have equality based on value a "Yes" value will be the same regardless of the module that is defining it, right? If that is the case how would one defined opaque types?

Maybe this will be related to the answer but I've seen the := operator defined in a few places but couldn't find anything regarding it on the tutorial. I feel like I'm missing something that would glue it all together.

view this post on Zulip Folkert de Vries (Sep 21 2022 at 13:30):

yes := is for making opaque types

view this post on Zulip Folkert de Vries (Sep 21 2022 at 13:31):

it's a bit like a newtype wrapper in haskell: you wrap a type in an opaque wrapper. so Foo := I64 creates a distinct type Foo that is not compatible with I64

view this post on Zulip Folkert de Vries (Sep 21 2022 at 13:31):

you can (un)wrap these with @Foo, e.g. @Foo intValue = @Foo 42

view this post on Zulip Georges Boris (Sep 21 2022 at 13:53):

Got it! I was trying to exercise my understanding and stress things out when dealing with opaque and "public" types with the same name.

I came up with this that I believe is wrong in multiple places but it got me thinking. The [Bool, ModuleA.Bool] would work? would a type alias of a custom type usually be spread when part of a larger custom type? or would I need to be explicit like [ True, False, ModuleA.Bool ]?

# ModuleA.roc
Bool := [True, False]

truthy : Bool -> Str
truthy = \@Bool bool ->
  when bool is
    True -> "True"
    False -> "False"

# ModuleB.roc
truthy : [Bool, ModuleA.Bool] -> Str
truthy = \value ->
  when value is
    True -> "True"
    False -> "False"
    ModuleA.Bool -> ModuleA.truthy value

view this post on Zulip Folkert de Vries (Sep 21 2022 at 13:55):

you still need tags. so [ Bool, ModuleA.Bool ] does not really make sense, you'd need some [ A Bool, B ModuleA.Bool ]

view this post on Zulip Georges Boris (Sep 21 2022 at 14:08):

ah - got it, so this custom type is not recursive it only takes explicit types inside it (with params or not)

[ True, False, Maybe, DontKnow ] ✅

Bool : [ True, False ]
IndecisiveBool : [ Maybe, DontKow ]
[ Bool, IndecisiveBool ] 🔴

# since it would evaluate to
[ [ True, False ], [ Maybe, DontKnow] ]

# same as this wouldn't work:
[ Str, Int ]

# non-custom types need to be wrapped:
[ A Bool, B Indecisive, S Str, I Int ]

view this post on Zulip Georges Boris (Sep 21 2022 at 14:08):

kinda obvious now! thanks for clarifying! :)

view this post on Zulip Wolfgang Schuster (Sep 21 2022 at 15:13):

@Georges Boris I've been playing with porting elm-units & elm-geometry to Roc and have been using := quite a bit. If you'd like examples you can check out the code here https://github.com/wolfadex/roc/tree/wolfadex/roc-geometry/examples/roc-geometry. I've had to do a few refactors to not run into cyclic imports too, which makes for code that's definitely a little different from the Elm approach.

view this post on Zulip Wolfgang Schuster (Sep 21 2022 at 15:15):

The main thing being that in Elm you can have type Foo a b = Foo a but that's not allowed in Roc. The closest you can get in Roc is

Bar a : [ Bar a ]

Foo a b := Bar a

and then you have to wrap/unwrap Foo in many places.

view this post on Zulip Georges Boris (Sep 21 2022 at 15:49):

@Wolfgang Schuster uhnn why can't you Foo a b := [ Bar a ] directly?

view this post on Zulip Wolfgang Schuster (Sep 21 2022 at 15:58):

From my understanding of compiler errors @Foo can only be written in the file it's created in

view this post on Zulip Wolfgang Schuster (Sep 21 2022 at 16:00):

E.g. in my Types.roc I have

Point3d a units coordinates := { x : Frac a, y : Frac a, z : Frac a }

toPoint3d = \args -> @Point3d args

fromPoint3d = \@Point3d args -> args

I was struggling to find a way to directly create a value of type Point3d outside of that file.

view this post on Zulip Georges Boris (Sep 21 2022 at 16:02):

Yeah I believe you would need to provide constructor and getter functions like those but I believe it would be the same in Elm, right?

If you have an opaque type Money currency = Money Int you would only be able to pattern match it inside the module.

view this post on Zulip Brendan Hansknecht (Sep 21 2022 at 16:04):

If you want to create them outside of the file, why are you making them private tags/opaque types with :=? Can you just use :?

view this post on Zulip Wolfgang Schuster (Sep 21 2022 at 16:08):

Not exactly. In Elm, taking the example of elm-geometry, I can have

module Internal exposing (Money(..))

type Money currency = Money Int
--
module External exposing (dollars, Money)

import Internal exposing (Money(..))

type alias Money currency = Internal.Money currency

dollars i = Internal.Money i

and in my elm.json only expose External.elm. With this, Internal.Money is only available within my package and External.Money is available externally. Within my package, any module can create Internal.Money but users of the package can only create External.Money through functions I've exposed.

view this post on Zulip Georges Boris (Sep 21 2022 at 16:13):

ooohh got it - is Roc's package ergonomics already in place? Maybe this is just a pain point of working in the current situation where you're basically just importing files local to your project? (I have no idea)

view this post on Zulip Brendan Hansknecht (Sep 21 2022 at 16:15):

Currently everything is scoped to the module (file) and there is no package scope, so yeah, that may be the root cause of the pain point here.

view this post on Zulip Wolfgang Schuster (Sep 21 2022 at 16:36):

Partially, but it's easy to pretend for now. The bigger challenge is that in Elm you can do type Money currency = Money Int but that form isn't possible in Roc. I'm not sure it's necessary as writing Money currency := Int plus toMoney : Int -> Money currency and fromMoney : Money currency -> Int is pretty easy to write.

view this post on Zulip Richard Feldman (Sep 21 2022 at 16:55):

so we do have this at the platform level

view this post on Zulip Richard Feldman (Sep 21 2022 at 16:55):

e.g. this is how we get a phantom type in Task for the effect type

view this post on Zulip Richard Feldman (Sep 21 2022 at 16:55):

https://github.com/roc-lang/roc/blob/b081acaf3c21deeff0dc696bfba0230e71c3b051/examples/interactive/cli-platform/Task.roc#L5

view this post on Zulip Richard Feldman (Sep 21 2022 at 16:56):

we have an InternalTask module which follows this pattern

view this post on Zulip Richard Feldman (Sep 21 2022 at 16:56):

however, we don't have a "package that's not a platform" yet

view this post on Zulip Richard Feldman (Sep 21 2022 at 16:56):

just because it hasn't been implemented yet!

view this post on Zulip Richard Feldman (Sep 21 2022 at 16:56):

but maybe this is the first use case where we actually want that

view this post on Zulip Richard Feldman (Sep 21 2022 at 16:57):

and yeah, we just do to/from in the Internal module :big_smile:

https://github.com/roc-lang/roc/blob/b081acaf3c21deeff0dc696bfba0230e71c3b051/examples/interactive/cli-platform/InternalTask.roc

view this post on Zulip Wolfgang Schuster (Sep 21 2022 at 16:58):

This is definitely a case where it doesn't fit a platform and better fits a package. I was definitely going in with the assumption of "this will change when packages become a thing"

view this post on Zulip Wolfgang Schuster (Sep 21 2022 at 17:02):

I'm somewhat curious about how common this type of structure is in Elm. My gut says that it's only a thing because of cyclic imports, at least from the packages I use frequently in Elm.

view this post on Zulip Georges Boris (Sep 21 2022 at 17:16):

I use the "internal to package only" strategy extensively for elm-book (and elm-admin). I think these "applications as libraries" will use them a lot.

view this post on Zulip Richard Feldman (Sep 21 2022 at 17:17):

I really like it as a strategy

view this post on Zulip Richard Feldman (Sep 21 2022 at 17:17):

it's like an extra layer of modularity

view this post on Zulip Richard Feldman (Sep 21 2022 at 17:17):

in Roc the idea is to be able to import packages locally as part of any project, without publishing them, largely to enable this pattern for application development!

view this post on Zulip Wolfgang Schuster (Sep 21 2022 at 18:01):

import packages locally as part of any project, without publishing

Similar to how Deno works?

view this post on Zulip Richard Feldman (Sep 21 2022 at 18:03):

I dunno how Deno does it, but basically just like how Elm does it except you can specify a local (relative) filesystem path instead of e.g. a published package name like rtfeldman/elm-css

view this post on Zulip Richard Feldman (Sep 21 2022 at 18:04):

so I could say like "./my-package-name/main.roc" and then that package just gets imported


Last updated: Jul 06 2025 at 12:14 UTC