Stream: beginners

Topic: Confusion about Int Types


view this post on Zulip Ryan Bates (Dec 07 2023 at 18:18):

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.

  1. I've seen a shorthand version (Nat) and a longhand version (Int Natural), are these considered the same? Why have both?
  2. Does Roc ever do type casting behind the scenes? If a function expects I64 and I give it I32, does it cast it?
  3. I noticed if I type 123 in the console it outputs Num * type. Is this a wildcard type that is casted as needed?

view this post on Zulip Richard Feldman (Dec 07 2023 at 18:32):

great questions!

view this post on Zulip Richard Feldman (Dec 07 2023 at 18:32):

Ryan Bates said:

  1. 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

view this post on Zulip Richard Feldman (Dec 07 2023 at 18:37):

Ryan Bates said:

  1. I've seen a shorthand version (Nat) and a longhand version (Int Natural), are these considered the same? Why have both?
    [...]

  2. I noticed if I type 123 in the console it outputs Num * 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:

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))

view this post on Zulip Richard Feldman (Dec 07 2023 at 18:38):

when you put a plain number into the repl, it gets the most general type possible. For example:

this is why you can do things like someFraction + 1

view this post on Zulip Richard Feldman (Dec 07 2023 at 18:39):

because 1 is a Num * and someFraction is some sort of Frac (which is a type alias for some Num (Fraction ...))

view this post on Zulip Richard Feldman (Dec 07 2023 at 18:39):

so they'll be compatible

view this post on Zulip Richard Feldman (Dec 07 2023 at 18:39):

btw I used Fraction Decimal instead of Integer Natural above because Nat is going to be removed from the language (replaced by just U64)

view this post on Zulip Richard Feldman (Dec 07 2023 at 18:39):

but the same ideas apply!

view this post on Zulip Richard Feldman (Dec 07 2023 at 18:40):

happy to answer any follow-up questions about that, or rephrase if anything was unclear :smiley:

view this post on Zulip Ryan Bates (Dec 07 2023 at 18:56):

Thanks @Richard Feldman, that helps a lot. A couple more questions.

  1. Do you have any recommendations when working with these types in code if you aren't concerned about performance? For example, is it better to use Num * everywhere in type definitions so it stays flexible? Or is it better to be explicit?
  2. Is it possible to cast an explicit type back into a generic type? For example, if I do 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.

view this post on Zulip Richard Feldman (Dec 07 2023 at 18:59):

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

view this post on Zulip Richard Feldman (Dec 07 2023 at 19:00):

in function types, I default to whatever is most flexible because it's usually more about the logic than the data type

view this post on Zulip Richard Feldman (Dec 07 2023 at 19:01):

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

view this post on Zulip Richard Feldman (Dec 07 2023 at 19:02):

and putting concrete number types in data structure type aliases means those type aliases don't need type variables

view this post on Zulip Richard Feldman (Dec 07 2023 at 19:03):

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:

view this post on Zulip Ryan Bates (Dec 07 2023 at 19:08):

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.

view this post on Zulip Elias Mulhall (Dec 07 2023 at 20:17):

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

If 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.

view this post on Zulip Elias Mulhall (Dec 07 2023 at 20:24):

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.

view this post on Zulip Brendan Hansknecht (Dec 07 2023 at 20:30):

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

view this post on Zulip Ryan Bates (Dec 07 2023 at 20:47):

@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.

view this post on Zulip Elias Mulhall (Dec 07 2023 at 20:55):

:+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.

view this post on Zulip Elias Mulhall (Dec 07 2023 at 20:55):

That said I haven't done day 7 or even really looked at it

view this post on Zulip Ryan Bates (Dec 07 2023 at 21:54):

@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