Passing a block to a class constructor, invoking with self yield inside the constructor and trying to access an instance method errors with:
$ crystal block.cr
Error in block.cr:11: instantiating 'Foo:Class#new()'
Foo.new do
^~~
in block.cr:11: instantiating 'Foo:Class#new()'
Foo.new do
^~~
in block.cr:12: undefined local variable or method 'bar'
bar
^~~
$ crystal block.cr
bar
class Foo
def initialize(&blk)
with self yield
end
def bar
puts "bar"
end
end
Foo.new do
bar
end
$ crystal -v
Crystal 0.22.0 (2017-04-20) LLVM 4.0.0
It's a known limitation. I don't know how to fix it, but PRs are welcome.
Workaround:
class Foo
def self.new
instance = new
with instance yield
instance
end
def bar
puts "bar"
end
end
Foo.new do
bar
end
@asterite what is the actual limitation here?
I have a similar issue :
Not working code :
class Button
class Config
def initialize(@button : Button)
end
def on_click(&block : -> Void)
@button.set_on_click &block
end
end
@on_click = -> {}
def initialize
with Config.new(self) yield
end
protected def set_on_click(&block)
@on_click = block
end
def on_click
@on_click.call
end
end
button = Button.new do
on_click { pp "Hello World" }
end
button.on_click
Error in line 26: instantiating 'Button.class#new()'
in line 13: too many block arguments (given 1, expected maximum 0)
Working work-around
class Button
class Config
def initialize(@button : Button)
end
def on_click(&block : -> Void)
@button.set_on_click &block
end
end
@on_click = ->{}
def self.create
btn = new
with Config.new(btn) yield
btn
end
protected def set_on_click(&@on_click)
end
def on_click
@on_click.call
end
end
button = Button.create do
on_click { pp "Hello World" }
end
button.on_click
"Hello World"
$ crystal -v
Crystal 0.27.2 [60760a546] (2019-02-05)
LLVM: 4.0.0
Default target: x86_64-unknown-linux-gnu
$uname -a
Linux Keios 4.14.102-1-MANJARO #1 SMP PREEMPT Wed Feb 20 22:55:55 UTC 2019 x86_64 GNU/Linux
The limitation is what new method to generate when with... yield is used. If you can come up with something then it should be easy to implement.
I don't know how with ... yield is implemented, and I'm shared between the promises of https://forum.crystal-lang.org/t/rfc-with-yield-replacement/386 and diving into the impl of with yield..
I mean, every initialize also defines a self.new method. The definition is allocate, call initialize, return the instance. What would the self.new look like if the initialize had a with...yield in it? I proposed one expansion in one of the comments here, but that's not always the expansion that people want (the Config example above, for example)
Oh I see.. I think that would be solved by the with .. yield replacement from the forums by @bcardiff (btw I really like your &.{ ... } suggestion).
In the meantime I think we should at least display a proper error message, and workaround explanation.
I'm assuming its also a limitation that you can't use with self yield to set ivars? (Something like this)
https://play.crystal-lang.org/#/r/8930
class Foo
property host : String
property allow_creds : Bool = false
def initialize(& : self -> Nil)
with self yield
end
end
foo = Foo.new do
host = "google.com"
end
Error: instance variable '@host' of Foo was not initialized directly in all of the 'initialize' methods, rendering it nilable. Indirect initialization is not supported.
EDIT: Ofc this isn't going to work, just is an example.
For now we should disallow it with a proper error, so that it can be allowed later
Most helpful comment
It's a known limitation. I don't know how to fix it, but PRs are welcome.
Workaround: