Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Defining a class method outside its definition #11764

Open
HertzDevil opened this issue Jan 23, 2022 · 2 comments
Open

Defining a class method outside its definition #11764

HertzDevil opened this issue Jan 23, 2022 · 2 comments

Comments

@HertzDevil
Copy link
Contributor

Crystal appears to copy Ruby's ability to define a class method outside a definition of that type, by using a type name as the receiver of a def instead of self:

module Foo
end

def Foo.foo
  1
end

Foo.foo # => 1

It even works if the target type is unrelated to the current definition scope:

module Foo
end

module Bar
  def Foo.foo
    1
  end

  module Foo; end

  # not a redefinition
  def Foo.foo
    2
  end
end

Foo.foo      # => 1
Bar::Foo.foo # => 2

I think this is confusing, as the those targets depend on whether Bar::Foo has been defined prior to the def, so I would like the compiler to reject the second case at least. There is less ambiguity in the first case because those type lookups always start from the top level.

Crystal itself uses the top-level defs extensively in src/json/from_json.cr and src/yaml/from_yaml.cr, and also for the Spec module. The only non-top-level defs already refer to the enclosing type:

crystal/src/float.cr

Lines 262 to 270 in 9e52dd6

# Returns a `Float64` by invoking `to_f64` on *value*.
def Float64.new(value)
value.to_f64
end
# Returns a `Float64` by invoking `to_f64!` on *value*.
def Float64.new!(value) : Float64
value.to_f64!
end

@beta-ziliani
Copy link
Member

Is there a valid use case for redefining another class's method within another type scope? (e.g., first def in Bar). To me it sounds like we can keep the top-level re-definition (which saves you from module Foo; def foo; ...; end; end), and consider the second case as a re-definition of Bar::Foo.

@HertzDevil
Copy link
Contributor Author

One application I recently found is being able to do this without #8422:

require "json"

# okay, `NoReturn.from_json` will deserialize to a "value" of type NoReturn
def NoReturn.new(pull : JSON::PullParser) : NoReturn
  pull.raise "Cannot deserialize to NoReturn"
end

This then allows one to write:

struct Foo
  include JSON::Serializable

  # okay, this is a formally correct way to specify that `x` must be an empty array
  getter x : Array(NoReturn)
end

Foo.from_json %({"x":[]})  # => Foo(@x=[])
Foo.from_json %({"x":[1]}) # Cannot deserialize to NoReturn at line 1, column 8 (JSON::SerializableError)

The alternative, which might look even odder to some eyes, is:

alias NoReturnClass = NoReturn.class

class NoReturnClass
  def new(pull : JSON::PullParser)
    pull.raise "Cannot deserialize to NoReturn"
  end
end

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants