When compiling the following code:
class Test
def fn
with self yield
end
end
Test.new.fn do
puts self
end
Why do I get the error there's no self in this scope? How can I implement DSLs in the absence of self?
Suffice it to say that any attempt to call self-local methods within the block also fails, naturally.
Methods called in such a block do work: http://play.crystal-lang.org/#/r/9kf
As long as the methods aren't protected or private, there shouldn't be anything preventing you from implementing a DSL with with self yield.
Actually, I think it's the initialize method at fault:
class TestGroup
def initialize
with self yield
end
def test()
end
end
TestGroup.new do
test
end
Yeah pretty much. Look at this... http://play.crystal-lang.org/#/r/9km
So why doesn't this work?
http://play.crystal-lang.org/#/r/9kn
I honestly don't know! Perhaps @asterite could enlighten us :)
@stugol @Exilor Acording to the docs:
A yield expression can be modified, using the with keyword, to specify an object to use as the default receiver of method calls within the block
That is, self isn't changed. The only thing that with obj yield gives you is that when you do foo(...) (without an explicit receiver), that foo method is first looked up in obj.
self at the top-level doesn't exist (unlike Ruby), so that's the error.
@stugol What DSL are you trying to build?
And the explanation to why self isn't changed, is because we (me and others) stumbled across too many bugs where you had:
class Foo
def bar
# This method is executed via instance_eval, but maybe you don't know that
some_obj.some_innocent_method do
puts @x # oops, @x is not Foo's @x
puts self # oops, self is not Foo
end
end
@asterite I still don't understand, however, why the code at http://play.crystal-lang.org/#/r/9kn doesn't work.
I'm trying to write a testing framework, based on my Ruby one. And then, I'm trying to write a smart client/server delta-based backup program in Crystal ;)
Why? Because C++ seems to be designed for the _express purpose_ of annoying me.
@stugol You can do it like this.
The thing is, TestGroup.new do ... end doesn't change self inside the block. If you want to forward the object sent by with ... yield you also have to pass it as a block argument.
@asterite Why doesn't the group = new line cause infinite recursion?
Also, why is it necessary to with group yield group? Surely yield group would be sufficient here? Besides, if we're doing with group yield in new, why is it necessary to pass the parameter in any case? Is there some technical reason why with yield doesn't work inside new? What am I missing here?
Also, doesn't self.new have to return self?
@stugol group = new doesn't cause infinite recursion because it's calling the new without a block overload (methods also overload based on whether they receive a block or not). But this particular case is a bit controversial because if you don't define an "initialize" then the compiler will automatically define an empty one for you.
Yes, yield group is enough. But if you don't use with ... then inside the TestGroup.new do |group|, invoking test (wihtout a receiver) won't work. But if you don't need that, it's not needed.
with yield is working inside new, at least in my example it does.
I don't understand the "self.new has to return self" question.
@asterite Ah, I see what you mean. Without with group yield there is no "implicit self" so I can't call DSL methods in my block; and without yield group there is no "explicit self" so I can't with self yield inside my block. Gotcha.
So, essentially, with yield never provides an actual self value to the block - you always have to pass it as a parameter to the block if you need to refer to it? Unlike Ruby. Why did you decide to do things that way?
As to the other question....doesn't the new method have to return the new object to the caller? Otherwise value = TestGroup.new wouldn't work. Or does it implicitly return itself?
Incidentally... I don't fully understand the &block syntax. I understand it in Ruby, but in Crystal it's a bit confusing. Given the following code:
def fn(&block)
yield if block
end
Firstly, is it valid to query the existence of the block in this manner? Secondly, can I call it without a block? Thirdly, does Crystal have a block_given??
def fn
yield if block_given?
end
I'm a bit confused about captured- vs non-captured blocks as well.
Unlike Ruby. Why did you decide to do things that way?
https://github.com/manastech/crystal/issues/1095#issuecomment-127098048
@stugol It's all in the docs. Please read all the sections.
I know things aren't exactly like Ruby, but that's the price we have to pay if we want a statically typed and checked language.
@ozra that's not a reason. Ruby does it that way, and seems to work perfectly fine. Why should bugs in Crystal be any more likely?
@asterite I have read the docs.
@stugol ...because Ruby code can occasionally be an insane bug magnet?
Coming from Python, the reason I use Crystal and not Ruby is _because_ Crystal is so much more strict, so those weird errors don't happen.
@stugol Ruby allows you to invoke non-existent methods at runtime, and you get an exception. In Crystal we chose not being able to do that. So because Ruby does some things in a way shouldn't mean we should do exactly the same.
I'll close this one. The language works as expected here and for now there are no intentions of changing the behaviour of with ... yield. And you can do what you want if you forward the scope in a block argument.
Most helpful comment
@asterite I still don't understand, however, why the code at http://play.crystal-lang.org/#/r/9kn doesn't work.
I'm trying to write a testing framework, based on my Ruby one. And then, I'm trying to write a smart client/server delta-based backup program in Crystal ;)
Why? Because C++ seems to be designed for the _express purpose_ of annoying me.