Shouldn't this work?

This code compiles|runs fine.

def powmodint(b, e, m)
  r = typeof(m).new(1)
  b = b % m;  b = typeof(m).new(b)
  while e > 0
    r = (r &* b) % m if e.odd?
    e >>= 1
    b = (b &* b) % m
  end
  r
end

def powmod(b, e, m)
  if m > UInt64::MAX
    r = powmodgmp(b, e, m)
  elsif m <= UInt32::MAX
    r = powmodint(b, e, m.to_u64)
  else
    r = powmodint(b, e, m.to_u128)
  end
end

When I shorten it to this:

def powmodint(b, e, m)
  r = typeof(m).new(1)
  b = b % m;  b = typeof(m).new(b)
  while e > 0
    r = (r &* b) % m if e.odd?
    e >>= 1
    b = (b &* b) % m
  end
  r
end

def powmod(b, e, m)
  if m > UInt64::MAX
    r = powmodgmp(b, e, m)
  else
    m = (m <= UInt32::MAX) ? m.to_u64 : m.to_u128
    r = powmodint(b, e, m)
  end
end

It gives this error:

In primesutilstestyb15c_ctznew1b2.cr:22:17

 22 | r = typeof(m).new(1)
                    ^--
Error: wrong number of arguments for '(UInt128 | UInt64).new' (given 1, expected 0)

Overloads are:
 - Union(*T).new()

I think this should not be a compiler error, and no worse than a warning.

I understand why at compile m is considered a Union type.
But there’s no runtime paradox, as m will collapse to be one type in powmodint.

If the first code form compiles with no errors why shouldn’t the second?

What arguments are you giving those methods that causes a compiler error?

Edit: Asking because it compiles and runs just fine for me when passing all arguments as Int32 and Int64s.

Values are Uints or BigInts.

I’m still not seeing what you’re seeing. Can you paste some code that reproduces the error?

It looks quite predictable. In your first code snippet, the method powmodint receives a third parameter of m.to_u64 or m.to_u128, which is uniquely determined and known by the compiler when enter the elseif branch. In the second snippet, the type of variable m needs to be determined at runtime thus recognized as a union. It could be a solution to use m.class instead of typeof(m) to avoid this issue.

The following simple code may help explain something.

m = rand > 0.5 ? 1 : 1.01
puts typeof(m) # => (Float64 | Int32)
puts m.class # => Float64

in this code, m is a union. a union does not mean it is either of the types, but a C-style union that you need to unwrap to read the value. Union class does not have the methods you expected it to have.

I am saying the compiler’s heuristics can be made simpler,
and more intelligent, to understand how to correctly deal with this situation.

The process can go something like this.

Here:

m = (m <= UInt32::MAX) ? m.to_u64 : m.to_u128

the value of m doesn’t change.
m will be a UInt type of its orignal value no matter which branch is taken.

m is not, nor can ever be, a Union of two types at runtime.

m is only used here.

def powmodint(b, e, m)
  r = typeof(m).new(1)
  b = b % m;  b = typeof(m).new(b)
  while e > 0
    r = (r &* b) % m if e.odd?
    e >>= 1
    b = (b &* b) % m
  end
  r
end

m is used to perform numerical computations with.

As far as the compiler should be concerned, the above code should
pose no conflicts, or create unresolved paradoxes, for the given heuristics.
(In fact, if you make the types in both branches the same it compiles.)

Now for this example:

m = (m <= UInt32::MAX) ? m.to_u64 : m.to_s

This should return a compiler error because m is no longer
assured to be an Int type which can perform numeric computations.

Thus I am saying it is algorithmically too rigid for the compiler
to label m to be a Union of two types here, when the heuristics
of the runtime clearly state m is assured to be just one Int type.

At the worse, maybe the compiler should issue a warning to identify
a possilbe runtime conflict, but that should be all that is necessary.

Reminder that typeof as you’re using represents the compile time type which in your case absolutely results in a union. If you want the runtime type you should use .class like @Sunrise suggested. That’s probably what you want to be using instead of typeof.

1 Like

Thank you, Thank you, Thank you!!

This now works.

def powmodint(b, e, m)
  r = m.class.new(1)
  b = b % m;  b = m.class.new(b)
  while e > 0
    r = (r &* b) % m if e.odd?
    e >>= 1
    b = (b &* b) % m
  end
  r
end

def powmod(b, e, m)
  if m > UInt32::MAX
    r = powmodgmp(b, e, m)
  else
    m = (m <= UInt32::MAX) ? m.to_u64 : m.to_u128
    r = powmodint(b, e, m)
  end
end

I never knew the compiletime vs runtime distinction here.
In fact, I never knew (or seen used and explained) the use of x.class.

I’m glad I asked this question, because I’ve learned a nuance of coding in Crystal that I never saw explained in a teachable manner.

These kind of nuggets of knowledge of Crystal programming really need to be shown and explained in the docs and tutorials to teach people how to use the language as productively as possible.

1 Like

https://crystal-lang.org/reference/syntax_and_semantics/typeof.html

the value of m doesn’t change.

It does change:

In the first case it’s either an UInt64 or and UInt128 and you invoke powmodint(UInt64) or powmodint(UInt128) (crystal will generate two distinct methods). The underlying value is the number itself.

In the second case it becomes an union UInt64 | UInt128 and you invoke powmodint(UInt64 | UInt128) (crystal will generate a single method). The underlying value is an union represented as {type, number}.

As a user of the language please listen to how I am experiencing it.

The compiler failed to compile the original code for stated reasons that were unclear.

The given error didn’t say what the real problem was, nor suggest a solution.

The Rust compiler may have said something like:

m is a compiletime Union which can't be resolved in powmodint at runtime.
Change typeof(m).new() to m.class.new() to resolve it at runtime.

To @straight-shoota

This document on typeof

https://crystal-lang.org/reference/syntax_and_semantics/typeof.html

makes no mention of the difference and nuances of its use and affects at compiletime vs runtime. No reference to use .class instead of typeof to resolve runtime use case differences is mentioned.

To @ysbaddaden

Your statement seems overly argumentative.

The value of m never changes, just its class representation.
Anyone objectively reading what I wrote could glean the point I was making.

The fact that @Sunrise and @Blacksmoke16 provided the solution indicates they understood what I was getting at.

Look, learning languages (human or software) is hard, coming from another|others.

I appreciate all the work that goes into making Crystal this good so far.

But if users aren’t given the information – from their perspective – to become efficient and productive in it, they can become frustrated and disenchanted, and eventually give up on it.

I keep bringing up Rust, because that community knows it’s a very hard language to learn, and get your head around to. That’s why they have one of the best compilers around, that not only tells you what’s wrong, but also how to fix it. They also have exstensive and detailed online documentation, and forum, to help users.

Documentation|Tutorials are a perennial issue raised in the Crystal forums. As I and others have stated many times, like writing good compilers|code, writing good|clear|useful documentation is a skill|art. Ideally, Devs should not be writing documentation. That should be done by people who know how to do that best.

I totally realize Crystal currently doesn’t have the money|people|resources to do any of this well.

But…you can dream and plan to, can’t you?

FWIW It’s talked a bit about in Union types - Crystal. Probably wouldn’t hurt to mention it in the other page as well.

It’s obvious you didn’t read the document carefully.

https://crystal-lang.org/reference/1.14/syntax_and_semantics/union_types.html

Although, doc organization always feels a bit messy and hard to search, you have to read it from beginning to end to avoid missing some details.


EDIT: i didn’t saw previous answer before I answer my question, in fact, when I forget some concepts but I know it mentioned somewhere in document, I always search in the official crystal-book git repo use rg.

1 Like