Stream: beginners

Topic: Alternative to Int *


view this post on Zulip Jonathan (Jun 25 2026 at 15:06):

I'm working on porting roc-random and came across list which is of type

list : Generator a, Int * -> Generator (List a)

The Int * is used to generate "n" random numbers. For comparison, Roc now uses the following signature for List.repeat:

repeat : a, U64 -> List(a)

This seems quite specific and, e.g., won't accept U32 without .to_u64 first. Equally, I'm not sure

repeat : a, n -> List(a) where [n.to_u64 : n -> u64]

is the clearest signature. After all, I don't think U64 currently implements to_u64.

view this post on Zulip Jonathan (Jun 25 2026 at 15:10):

I'm still not sure how numeral parsing works, but I'm guessing there's also an argument to be made to restrict it to something like the below because that is what is needed, but it doesn't communicate as clearly to me that "This should be an unsigned integer" and also leaks the implementation. If the function is changed so that the destination list is reserved ahead of time, n must now implement also n.to_u64, so that List.with_capacity can be used, which then excludes using U64 itself.

list : Generate(a), n -> Generate(List(a))
  where [
    n.to : n, n -> Iter(n),
    # something that allows us to parse '0' to type n so that we can do 0.to(n)
]

view this post on Zulip Jonathan (Jun 25 2026 at 15:12):

I haven't got to testing it yet, and I could probably leave it without a signature and it will Just Work, but I don't know what the best signature would be for e.g., someone reading documentation. Probably U64? Unless U64 and other numerals come to implement redundant [UI][n].to_[ui][n] methods.

view this post on Zulip Jonathan (Jun 25 2026 at 15:18):

(Side note - is there a way to inspect the type of something like _ type holes? I think before I've just put a blatantly wrong annotation and looked for the compiler error)

view this post on Zulip Anton (Jun 26 2026 at 12:08):

Jonathan said:

(Side note - is there a way to inspect the type of something like _ type holes? I think before I've just put a blatantly wrong annotation and looked for the compiler error)

You can use the LSP but it still has some bugs.

view this post on Zulip Jonathan (Jun 26 2026 at 13:47):

Unless U64 and other numerals come to implement redundant [UI][n].to_[ui][n] methods.

I'll set it as U64 then :+1: Would it make sense to have these? Just so that whenever you need an integer you can get anything that can be turned into it including itself?

view this post on Zulip Anton (Jun 26 2026 at 14:24):

It seems reasonable. What do you think @Richard Feldman? To summarize, the proposal is to add e.g. to_u64 to U64, so that you can make a function with where [n.to_u64 : n -> U64].

view this post on Zulip Richard Feldman (Jun 26 2026 at 14:25):

yep, sounds good! :thumbs_up:

view this post on Zulip Richard Feldman (Jun 26 2026 at 14:25):

same for the other number types

view this post on Zulip Anton (Jun 26 2026 at 14:25):

Good, I will make an issue

view this post on Zulip Richard Feldman (Jun 26 2026 at 14:25):

actually, sorry - we should not add it to I128 or U128 or I64

view this post on Zulip Richard Feldman (Jun 26 2026 at 14:26):

because those are lossy conversions

view this post on Zulip Richard Feldman (Jun 26 2026 at 14:26):

those should have to_u64_try

view this post on Zulip Jonathan (Jun 26 2026 at 14:26):

But each up to their own n right?

view this post on Zulip Richard Feldman (Jun 26 2026 at 14:26):

yeah, taking into account signed vs unsigned

view this post on Zulip Richard Feldman (Jun 26 2026 at 14:27):

e.g. I64.to_u64 and U64.to_i64 shouldn't exist because they are lossy

view this post on Zulip Richard Feldman (Jun 26 2026 at 14:27):

the "just trust me I know what I'm doing" versions would be I64.to_u64_wrap and U64.to_i64_wrap

view this post on Zulip Anton (Jun 26 2026 at 14:29):

For clarity, we already have the wrap functions.

view this post on Zulip Anton (Jun 26 2026 at 14:37):

#9830

view this post on Zulip Anton (Jun 26 2026 at 14:39):

Do we want to do to_<self>_try as well?

view this post on Zulip Richard Feldman (Jun 26 2026 at 16:32):

sure, just have it return Try(MySpecificType, _never_fails)

view this post on Zulip Jonathan (Jun 29 2026 at 22:25):

As these are all no-ops, should these be done in Roc or piped through low level and codegen?

view this post on Zulip Richard Feldman (Jun 30 2026 at 00:29):

I think they can just be pure Roc since all they do is return self or Ok(self) :smile:

view this post on Zulip Jonathan (Jun 30 2026 at 17:04):

One other thing - I just realised to_str was not defined for Str. Shall I tack it onto the PR while I'm at it?

view this post on Zulip Richard Feldman (Jun 30 2026 at 17:06):

sure!

view this post on Zulip Jonathan (Jul 01 2026 at 12:27):

This is a continuation of a similar problem, so adding it on here. I'm midway through converting roc-random and it's a little tricky doing it faithfully in some cases that depended on generic Int behaviour.

between_unsigned : Int a, Int a -> Generator (Int a)
between_unsigned = |x, y|
    (minimum, maximum) = sort(x, y)
    range = maximum - minimum |> Num.add_checked(1)

    |s|
        # TODO: Analyze this. The mod-ing might be biased towards a smaller offset!
        value =
            when range is
                Ok(r) -> minimum + (Num.int_cast(permute(s))) % r
                Err(_) -> permute(s) |> Num.int_cast
        state = update(s)

        { value, state }

The lines I'm thinking about have Num.int_cast. permute returns a U32, and so we need a way of converting U32 -> generic "int". We have U32.to_u8_wrap, U32.to_u16_wrap etc., but in this case we don't know the return type and have no way to parameterise over it, nor have the dual to_ conversions <Num>.from_u32_wrap, <Num>.from_u16_wrap etc.

Ideas so far:

The fundamental shape of it is that we have a U32 core that needs to output generic integer types. Is there something obvious or more out of the box I'm missing?

view this post on Zulip Jonathan (Jul 01 2026 at 12:30):

I mean, there is one solution in passing in a from_u32 : U32 -> a function, but was wondering if there was a dispatch-y way to do it that mirrors the generic behaviour that Int * permitted before.

between_unsigned = |x, y, from_u32| {
    (minimum, maximum) = sort(x, y)
    range = (maximum - minimum).add_try(1)

    |s| {
        # TODO: Analyze this. The mod-ing might be biased towards a smaller offset!
        value = match range {
            Ok(r) -> minimum + from_u32(permute(s)).mod_by(r)
            Err(_) -> from_u32(permute(s))
        }
        state = update(s)

        { value, state }
    }
}

view this post on Zulip Anton (Jul 01 2026 at 12:56):

Looks like there may have been a communication issue, roc-random was migrated yesterday https://github.com/kili-ilo/roc-random/pull/51. Or perhaps it was not completely migrated?

view this post on Zulip Jonathan (Jul 01 2026 at 12:58):

Communication on my part :sweat_smile:

view this post on Zulip Jonathan (Jul 01 2026 at 13:01):

Ah well, still it's interesting to me that the solution there was to use between_u8, between_16 etc. and duplicate the function logic.

view this post on Zulip Luke Boswell (Jul 01 2026 at 21:18):

Sorry I didnt realise :smiling_face_with_tear:. I just launched into that while on a roll with path/http/ansi/parser. I havent spent a lot of time thinking about the API, it was very much my goal to get something working as a first pass and then polish it later.

view this post on Zulip Aurélien Geron (Jul 29 2026 at 01:37):

I'm porting roc-isodate and it uses Int * quite a lot. So far, I've ported this using the same approach as @Jonathan proposed at the beginning of this topic, for example, this old code:

from_ymdhmsn : Int *, Int *, Int *, Int *, Int *, Int *, Int * -> DateTime
from_ymdhmsn = |year, month, day, hour, minute, second, nanosecond|
    { date: Date.from_ymd(year, month, day), time: Time.from_hmsn(hour, minute, second, nanosecond) }

Now looks like this:

from_ymdhmsn : y, m, d, h, mi, s, n -> DateTime where [y.to_i64 : y -> I64, m.to_u8 : m -> U8, d.to_u16 : d -> U16, h.to_u8 : h -> U8, mi.to_u8 : mi -> U8, s.to_u8 : s -> U8, n.to_u32 : n -> U32]
from_ymdhmsn = |year, month, day, hour, minute, second, nanosecond| {
    { date: Date.from_ymd(year, month, day), time: Time.from_hmsn(hour, minute, second, nanosecond) }
}

Is this the recommended way to go now?

It's way more verbose than Int *, but I guess it's also more explicit. The main thing I dislike is the necessary duplication if you need the same logic for different integer types (as in the between_u8 / between_u16 case that @Jonathan pointed out). Tbh, it feels like a regression compared to the old compiler, but maybe it's not that common so it's not worth whatever compiler complexity it caused. Wdyt @Richard Feldman ?

view this post on Zulip Richard Feldman (Jul 29 2026 at 02:03):

personally I'd just have them take U8, U16, etc.

view this post on Zulip Richard Feldman (Jul 29 2026 at 02:04):

number literals will of course Just Work, and if I already have a number in a different format, I personally like the conversion being explicit because it tells me where I'm incurring a cost (if any), and might influence me to change my data model to line up with how my numbers are being used

view this post on Zulip Aurélien Geron (Jul 29 2026 at 02:16):

Makes sense. One related issue I've just run across is the Const module. The old version looks like this:

module [epoch_year, epoch_month, epoch_day, ...]

epoch_year = 1970
epoch_month = 1
epoch_day = 1
...

I initially ported it like this:

Const :: {}.{
    epoch_year = 1970
    epoch_month = 1
    epoch_day = 1
    ...
}

However, this caused issues down the line because the compiler defaults these values to Dec, but they're used as integers and the code tries to call .to_i64() or similar functions. So I had to modify the Const module like this:

Const :: {}.{
    epoch_year = 1970.I64
    epoch_month = 1.U8
    epoch_day = 1.U8
    ...
}

Perhaps 1970 should default to I64? Or maybe the compiler should somehow be smart enough to determine that in this case 1970 must be an integer since we're trying to call to_i64()?

view this post on Zulip Richard Feldman (Jul 29 2026 at 03:12):

ah, so this is an unusual case...without going down a deep rabbit hole of compiler and language design, we decided awhile ago that in situations like this (constants associated with a type) the constant's type can't be polymorphic (or else it causes nonobvious problems that are worse elsewhere), and if you wanted the usual "number gets inferred based on usage" behavior, it would have to be polymorphic

view this post on Zulip Aurélien Geron (Jul 29 2026 at 03:56):

Got it, thanks.


Last updated: Aug 12 2026 at 12:35 UTC