Stream: ideas

Topic: Num Integer Unsigned and Num Integer Signed


view this post on Zulip Brendan Hansknecht (Jan 31 2024 at 16:54):

Spinning off of #beginners > Code review?, there were a few messages (start, end) around how it would be nice for Num.absDiff to always return an unsigned number.

This has a few advantages:

  1. It has a wider range of outputs for signed types. Num.absDiff -1 Num.maxI32 would work.
  2. Related to 1, it will never panic
  3. It is more truthful about the output type. End users will know that negative outputs are impossible.

In the current Roc type system, Signed and Unsigned numbers are not usefully distinguishable in a related way. There is no way to know that U8 and I8 are related and transform from one to the other.

I think we should restructure the Num type to support signedness.

Current I8 looks like this:

Int range : Num (Integer range)
I8 : Int Signed8

I propose that we separate the signedness from the bitwidth. There are multiple ways to do this. The most directly probably being:

I8 : Int Signed Bits8

With that type, a user could write:

absDiff : Int sign bits, Int sign bits -> Int Unsigned bits

Of course we will need a Num.toSigned and Num.toUnsigned or similar to support this, but I think it would be a great help to the type system.

Thoughts?

view this post on Zulip Richard Feldman (Jan 31 2024 at 21:09):

I thought about this initially (it can be done with Int still having one type variable by using a record, e.g. Int { sign, bits }, so then other functions can still have Int a, Int a -> ...) but I ended up concluding it wasn't worth the complexity given how rarely it comes up

view this post on Zulip Richard Feldman (Jan 31 2024 at 21:09):

what other use cases would this have besides absDiff?

view this post on Zulip Brendan Hansknecht (Jan 31 2024 at 21:10):

I ran into it while fuzzing, but yeah it is pretty rare.

view this post on Zulip Brendan Hansknecht (Jan 31 2024 at 21:12):

I had a function where I wanted to be able to accept any unsigned integer as input.

view this post on Zulip Brendan Hansknecht (Jan 31 2024 at 21:13):

Cause the function would give totally incorrect results for signed numbers.

view this post on Zulip Richard Feldman (Jan 31 2024 at 21:14):

do you remember what it was for?

view this post on Zulip Brendan Hansknecht (Jan 31 2024 at 21:16):

Yeah, it was for generating a random integer in between two values from fuzz input. How the algorithm works, you essentially have to convert to unsigned, run the algorithm, then convert back to signed.

view this post on Zulip Brendan Hansknecht (Jan 31 2024 at 23:00):

Actual rust algorithm for reference: https://github.com/rust-fuzz/arbitrary/blob/803f2df52fabe3a630fb054b8d7e7e2795487971/src/unstructured.rs#L303-L373

view this post on Zulip Brendan Hansknecht (Jan 31 2024 at 23:01):

Note the to_unsigned and from_unsigned calls

view this post on Zulip Brendan Hansknecht (Jan 31 2024 at 23:10):

Also, looking at the impl in more detail now, What the rust author did to support this was implement a trait on all the rust integer types. Technically the Roc number types are opaque. So theoretically an ability could be used if new abilities where allowed to be added to any opaque type.

view this post on Zulip Richard Feldman (Feb 01 2024 at 00:28):

yeah I strongly want to avoid that though :big_smile:

view this post on Zulip Brendan Hansknecht (Feb 01 2024 at 00:29):

Oh, ik. Just noting how it was dealt with in rust and what a direct translation would be.

view this post on Zulip Brendan Hansknecht (Feb 01 2024 at 00:30):

Too bad I can just comptime match on a type variable.

view this post on Zulip Brendan Hansknecht (Feb 01 2024 at 00:31):

signed : Int a -> Bool
signed = \_ ->
    comptime when a is
        Signed8 | Signed16 | ... -> Bool.true
        Unsigned8 | Unsigned16 | ... -> Bool.false

view this post on Zulip Richard Feldman (Feb 01 2024 at 03:51):

with compile-time evaluation of constants that would Just Work

view this post on Zulip Brendan Hansknecht (Feb 01 2024 at 03:58):

Only if we add the ability to match on a type variable in general

view this post on Zulip Richard Feldman (Feb 01 2024 at 14:44):

oh I missed that part nm

view this post on Zulip Brendan Hansknecht (Feb 01 2024 at 15:39):

I guess if you wanted to keep at most one type variable for simplicity, it could be:

Int a : Num (Integer a)

SInt a : Num (Integer (Signed a))

UInt a : Num (Integer (Unsigned a))

Would something along those lines work?

view this post on Zulip Brendan Hansknecht (Feb 01 2024 at 15:40):

Of course we don't have to actually expose and SInt or UInt alias. It could be required to write

Int (Signed a)

view this post on Zulip Richard Feldman (Feb 01 2024 at 18:17):

yeah that could also work

view this post on Zulip Richard Feldman (Feb 01 2024 at 18:17):

but overall I think the bar for making such a fundamental type more complicated is higher than the number of motivating use cases right now :big_smile:

view this post on Zulip Richard Feldman (Feb 01 2024 at 18:18):

especially considering it's more work for the type checker

view this post on Zulip Brendan Hansknecht (Feb 01 2024 at 18:40):

Well, at least we have an implementation that might work if we even gain enough motivating use cases.


Last updated: Jun 16 2026 at 16:19 UTC