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.
yes :=
is for making opaque types
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
you can (un)wrap these with @Foo
, e.g. @Foo intValue = @Foo 42
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
you still need tags. so [ Bool, ModuleA.Bool ]
does not really make sense, you'd need some [ A Bool, B ModuleA.Bool ]
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 ]
kinda obvious now! thanks for clarifying! :)
@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.
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.
@Wolfgang Schuster uhnn why can't you Foo a b := [ Bar a ]
directly?
From my understanding of compiler errors @Foo
can only be written in the file it's created in
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.
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.
If you want to create them outside of the file, why are you making them private tags/opaque types with :=
? Can you just use :
?
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.
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)
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.
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.
so we do have this at the platform level
e.g. this is how we get a phantom type in Task
for the effect type
we have an InternalTask
module which follows this pattern
however, we don't have a "package that's not a platform" yet
just because it hasn't been implemented yet!
but maybe this is the first use case where we actually want that
and yeah, we just do to
/from
in the Internal
module :big_smile:
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"
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.
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.
I really like it as a strategy
it's like an extra layer of modularity
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!
import packages locally as part of any project, without publishing
Similar to how Deno works?
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
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