I have this stumpy_png shard in my project that I’m trying to update for Crystal 0.36.1, but I keep running in to this issue which I originally reported to the shard.
In src/stumpy_png/crc_io.cr:18:12
18 | @crc = Digest::CRC32.update(slice, @crc)
^------------
Error: undefined constant Digest::CRC32
I tried to break out the issue outside of my project to narrow down what was causing it, and I noticed something weird. Take this example:
require "digest" # <-- if I remove this line, it'll compile
require "digest/crc32"
class CrcIO < IO # <-- or don't inherit from IO
getter crc : UInt32
property size
def initialize
@crc = 0_u32
@size = 0
end
def read(slice : Bytes)
0
end
def write(slice : Bytes) : Nil
@crc = Digest::CRC32.update(slice, @crc)
@size += slice.size
end
def reset
@crc = 0_u32
end
end
It seems to be some combination of requiring digest, and inheriting from IO that causes the Digest::CRC32
to randomly disappear…
Now, in the shard, it never calls require "digest"
which means that the shard on its own compiles fine and specs run and pass. My guess is my Lucky app must have a require somewhere that’s causing this issue, but I haven’t found where or what yet. The other strange thing is, if I replace that require "digest"
with a require "digest/digest"
, it’ll also work. Unfortunately, the patch doesn’t seem to work in my app directly. As in, I can’t just throw a random require "digest/digest"
at the top of my app and have it work.
Anyone have any ideas?
This is probably the issue: crystal/io_digest.cr at master · crystal-lang/crystal · GitHub
I really think we should rename that to DigestIO. Then also MemoryIO and so on. I think it was a very bad decision to try to namespace everything…
1 Like
Yup is probably that.
Workaround:
def write(slice : Bytes) : Nil
--- @crc = Digest::CRC32.update(slice, @crc)
+++ @crc = ::Digest::CRC32.update(slice, @crc)
@size += slice.size
end
1 Like
I find it actually a bit surprising is the namespace lookup from an inherited scope which takes precedence over literal scope. Basically this:
class Bar
BAR = "::Bar"
end
class Baz
class Bar
BAR = "Baz::Bar"
end
end
class Foo < Baz
Bar::BAR # => "Baz::Bar"
end
It works like this in Ruby. And I suppose you would expect that mostly for constants for easy access to constant values defined in an ancestor.
But in the context of namespaces, it feels somewhat unexpected to me. Foo
isn’t in the Baz
namespace, it’s in top level namespace and thus I’d expect Bar
to mean ::Bar
not Baz::Bar
.
There’s probably no way to change this without losing consistency. This is an effect of types also being namespaces, so I guess we’ll have to accept it.
1 Like
Yeah, the lookup works exactly the same as in Ruby. Maybe it’s a bad thing :-)
The problem with it is that it allows type-hijacking. For example, I define a library:
class Foo
def self.foo
Bar.bar
end
end
class Bar
def self.bar
puts "Hello!"
end
end
Foo.foo # "Hello"
Then someone comes and does this:
class Foo
class Bar
def self.bar
puts "Bye!"
end
end
end
Then the Bar
I was referring to in Foo
is no longer the same type, and I get “Bye!” as an output.
This is not that bad if you don’t use the same names inside namespaces that are also part of an outer namespace.
Again, I was against naming IO::Memory
like that (MemoryIO
is better: its name is more readable and clear, there’s less typing, and there’s less chance of a name clash). Same with StringBuilder
, DigestIO
, etc. It really makes no sense that it’s called IO::Digest
… what is it, a digest? No! It’s a “digest IO”, so it should be called DigestIO
. What’s IO::Memory
? Is it a memory? No! Well, I’ll stop my rant now…
By the way, the way C# names things is exactly the same as what I’m saying here. They have MemoryStream
, they don’t have Stream.Memory
.
4 Likes
Thanks for all the info! That at least lets me move past my issue so I can continue the upgrades.
I suppose in a more positive way this could also be considered a feature for injecting mock implementations for tests. 
MemoryIO
is certainly better as a name than IO::Memory
. And there are similar cases like *::Builder
, *::Reader
, *::Writer
, *::Parser
.
But the problem is, if we rename all of them, there’s gonna be a whole lot more types in the top level namespace. We’d have DelimitedIO
, StapledIO
, FileDescriptorIO
, MemoryIO
, HeydumpIO
, SizedIO
, MultiWriterIO
just from the IO
namespace. Polluting the namespace may or may not be a huge issue. For the API docs it would definitely mean to lose context.
Before even considering to change anything about this, we would need to find a really better alternative. Otherwise it’s not worth any migration cost.
2 Likes