Yield with self does not work when calling operator methods

I am trying to create a DSL with operator methods. However, in a “with self yield” block, the operators don’t work.

Given following code:

class Adder
  getter ns : Array(Int32) = [] of Int32

  def add=(n : Int32)
    ns.push n
  end

  def min(n : Int32)
    ns.push (-1 * n)
  end

  def calc(&)
    with self yield
  end
end

adder = Adder.new
puts adder.ns
adder.add = 1
puts adder.ns
adder.calc do
  add = 2
end
puts adder.ns
adder.min(3)
puts adder.ns
adder.calc do
  min(4)
end
puts adder.ns

I get output


[1]
[1]
[1, -3]
[1, -3, -4]

But I expected


[1]
[1, 2]
[1, 2, -3]
[1, 2, -3, -4]

Is it possible to use operator methods in a “with self yield” block?

Think the problem here is add = 2 is ambiguous, so the compiler is assuming it’s a local variable assignment. If you want it to call the setter, you can do itself.add = 2. Tho really a more practical solution would be to use a more idiomatic method name, like def <<(n : Int32) and/or push to make it more clear and not involve assignment so compiler will know what to do.

1 Like

Thanks for your quick answer!

The “add” and “minus” was just an example. In my DSL, I’ve got a lot of configuration options that can be set by assignment operator. The idea is to set options that can occur only once by assignment (=), and options that can occur multiple times by <<.

I’ll see if there’s another operator I can use instead of = for options I can set once.

if use self.add = 2 in the above block, like this:

adder.calc do
  add = 2
end

Will get error:

Error: there's no self in this scope

if no self, what is the itself method returned?

https://crystal-lang.org/api/Object.html#itself-instance-method

The catch is that even tho there is an implicit receiver, the block is still executing at the top level, which there is no self in so the error is valid. #itself works because it returns self from the context of the object that was yielded. I.e. self in the block itself and self as returned by #itself are two different scopes, the former of which is invalid.

1 Like

Interesting, so, in the Crystal, a block never has it’s own (direct) self, even not in the top level namespace, right? it’s self share the self in the object context, you must get it from Object#itself, right?

Following is a example:

class Adder
  getter ns : Array(Int32) = [] of Int32

  def add=(n : Int32)
    ns.push n
  end

  def min(n : Int32)
    ns.push(-1 * n)
  end

  def calc(&)
    with self yield
  end
end

class A
  def initialize(@adder : Adder)
  end

  def foo
    @adder.calc do
      self.add = 2 # Error: undefined method 'add=' for A
    end

   pp! @adder.ns
  end
end

a = A.new(Adder.new)

a.foo

Have to change from self.add = 2 into itself.add = 2 to work.

If i guess correct, this is a REALLY BIG different with Ruby, I’m surprised it never mentioned in the document.

As mentioned by @Blacksmoke16 its setter which is causing the ambiguity. so straight-forward approach would be define a add method for code to work.

class Adder
  getter ns : Array(Int32) = [] of Int32

  def add=(n : Int32)
    ns.push n
  end

  def add(n)
    ns.push n
  end

  def min(n : Int32)
    ns.push (-1 * n)
  end

  def calc(&)
    with self yield
  end
end

adder = Adder.new
puts adder.ns
adder.add = 1
puts adder.ns
adder.calc do
  add 2
end
puts adder.ns
adder.min(3)
puts adder.ns
adder.calc do
  min(4)
end
puts adder.ns

output will be what you expected

[]
[1]
[1, 2]
[1, 2, -3]
[1, 2, -3, -4]