So far in AoC I've relied on type inference which has been going quite well (awesome job!) but running into some issues with integers. Some functions expect certain integer types. I have a few questions.
Nat
) and a longhand version (Int Natural
), are these considered the same? Why have both?123
in the console it outputs Num *
type. Is this a wildcard type that is casted as needed?great questions!
Ryan Bates said:
- Does Roc ever do type casting behind the scenes? If a function expects I64 and I give it I32, does it cast it?
no, the conversions always have to be explicit
Ryan Bates said:
I've seen a shorthand version (
Nat
) and a longhand version (Int Natural
), are these considered the same? Why have both?
[...]I noticed if I type
123
in the console it outputsNum *
type. Is this a wildcard type that is casted as needed?
The answers to these are related: this is how Roc represents the nesting relationship between different classifications of numbers.
the way it works is:
Num a
is an opaque type.Num (Fraction Decimal)
is the full type of Roc's decimal numberDec
is a type alias for Num (Fraction Decimal)
Frac a
is a type alias for Num (Fraction a)
so this is why when you have a function that expects a Frac *
, you can give it a Dec
and it Just Works - it's because Frac a
is a type alias for Num (Fraction a)
, and Dec
is a type alias for Num (Fraction Decimal)
(which is compatible with Num (Fraction a)
)
when you put a plain number into the repl, it gets the most general type possible. For example:
42
gets the type Num *
because the number 42 is compatible with all integers and all fractions0x42
gets the type Int *
because it's an integer literal0.42
gets the type Frac *
because it's a fractional literalthis is why you can do things like someFraction + 1
because 1
is a Num *
and someFraction
is some sort of Frac
(which is a type alias for some Num (Fraction ...)
)
so they'll be compatible
btw I used Fraction Decimal
instead of Integer Natural
above because Nat
is going to be removed from the language (replaced by just U64
)
but the same ideas apply!
happy to answer any follow-up questions about that, or rephrase if anything was unclear :smiley:
Thanks @Richard Feldman, that helps a lot. A couple more questions.
Num *
everywhere in type definitions so it stays flexible? Or is it better to be explicit?List.len
it is Nat
type which is unsigned. If I need it signed I can doNum.toI64
but it might be nicer to cast to Num *
so the result can be used more flexibly.in data structures (e.g. record types) I generally default to the concrete type, e.g. Dec
or U64
, unless there's some specific demand for it to be more flexible than that, which has been rare in my experience
in function types, I default to whatever is most flexible because it's usually more about the logic than the data type
in general I don't like type variables propagating all over the place unless they're actually being used in a significantly valuable way for multiple different types
and putting concrete number types in data structure type aliases means those type aliases don't need type variables
whereas in function signatures they don't have to propagate; if you use a more flexible type there, callers can give them types that are more concrete and it's fine :big_smile:
Just so I'm understanding, let's say I have a Dict with keys of U8
. Is it a good practice to add a wrapper function around Dict.get
that accepts Num *
and internally does Num.toU8
before passing to the dict? This way the function is generic but the data type is explicit.
It depends. Casting from a Num *
to a U8
means that you need to decide what happens when the number is not an integer between 0 and 255. You could
Err OutOfBounds
or something similarIf you do that with a wrapping function then you're asserting that you want to handle the number conversion the same way in all cases. You'll also have to give the function a name/docs that clearly explain the behavior.
The alternative is to be explicit that you're working with a DIct U8 Foo
and require the caller to convert numbers to U8
as appropriate.
Is there a particular AoC problem you're working on? I'd consider giving your types alias that describe your domain. When a function is about manipulating your problem domain then use that concrete type alias. When a function is just a general helper unrelated to your problem domain then use generic types.
If you have a Dict U8 Something
, I definitely wouldn't wrap it. You fundamentally have a concrete type.
I would only write a generic function if you might have support types.
For example you might write something like:
someFn : Dict (Num a) SomeVal, Num a -> SomeOutput
This works with any integer type if that makes sense for the function. No need for any casts
@Elias Mulhall I am doing Day 7 part 1 and using Parser chompUntil ' '
to grab the cards which converts to List U8
. I'm also doing Str.toScalars "AJKQT"
elsewhere that generates U32
. I've decided to convert everything to U64
so I have consistent types.
@Brendan Hansknecht that makes sense about using Num a
to match types instead of having a wrapper function with type casting. I like the approach.
:+1:
I'd consider doing
Card : U8
or
Card : U64
or whatever, depending on which representation you want to "be" a card. Then in your function signatures you can have like
parseInput : Str -> List Card
parseInput = \input ->
No need to make it generic, you're decided on what a card is and that's all you have to solve for.
That said I haven't done day 7 or even really looked at it
@Elias Mulhall thanks for the suggestion. Making a Card
type is a good idea. I'm still new to types so nice to see some examples.
Last updated: Jul 06 2025 at 12:14 UTC