やってみる

アウトプットすべく己を導くためのブログ。その試行錯誤すらたれ流す。

「演算子式」を読む

 論理演算はC言語風に書ける。

成果物

情報源

演算子

  • 代入
  • 自己代入
  • 多重代入
  • 範囲式
  • 条件式としての範囲式
  • and
  • or
  • not
  • 条件演算子

例:

1+2*3/4

プログラミングの利便のために一部のメソッド呼び出しと制御構造は演算子形式をとります。Rubyには以下にあげる演算子があります。

 以前どこかで「Ruby演算子はメソッド呼出の糖衣構文である」みたいなことが書いてあった。そのことを言っているのだろう。

高い   ::
       []
       +(単項)  !  ~
       **
       -(単項)
       *  /  %
       +  -
       << >>
       &
       |  ^
       > >=  < <=
       <=> ==  === !=  =~  !~
       &&
       ||
       ..  ...
       ?:(条件演算子)
       =(+=, -= ... )
       not
低い   and or

左の「高い」「低い」は演算子の優先順位です。例えば「&&」は「||」より優先順位が高いので、以下のように解釈されます。

a && b || c   #=> (a && b) || c
a || b && c   #=>  a || (b && c)

 ()で囲うと優先順位が上がり先に評価する。それを説明しないのは小学生レベルの常識だからか?

ほとんどの演算子は特別な形式のメソッド呼び出しですが、一部のものは言語に組み込みで、再定義できません。

再定義できる演算子(メソッド)

+@, -@ は単項演算子 +, - を表しメソッド定義などではこの記法を利用します。

|  ^  &  <=>  ==  ===  =~  >   >=  <   <=   <<  >>
+  -  *  /    %   **   ~   +@  -@  []  []=  ` ! != !~

これらの演算子式の定義方法についてはクラス/メソッドの定義/演算子式の定義を参照してください。

再定義できない演算子(制御構造)

演算子の組合せである自己代入演算子は再定義できません。

=  ?:  ..  ...  not  &&  and  ||  or  ::

 自己代入ってなに? 先に説明してくれ。+=などのことかと思ったんだが、書いてないし。

代入

例:

foo = bar
foo[0] = bar
foo.bar = baz

文法:

変数 '=' 式
定数 '=' 式
式`['expr..`]' '='
式`.'識別子 '='

代入式は変数などに値を設定するために用いられます。代入はローカル変数や定数の宣言としても用いられます。代入式の左辺は以下のいずれかでなければなりません。

変数

変数 `=' 式

左辺値が変数の場合、式を評価した値が変数に代入されます。

配列参照

1`[' 式2 ... `]' `=' 式n

式1を評価して得られるオブジェクトに対しての、式 2 から式 n までを引数とする []= メソッド呼び出しに変換されます。

例:

class C
    def initialize
        @ary = [0,1,2,3,4,5,6,7]
    end
    def [](i)
        @ary[i * 2]
    end
    def []=( i, v )
        @ary[i * 2] = v
    end
end
c = C.new
p c[3]      # c.[]( 3 )  に変換され、その結果は 6
p c[3] = 1  # c.[]=(3,1) に変換され、その結果は 1

 こんな実装は絶対にしないだろうけどね。コード例のおかげで書き方はわかった。

属性参照

1 `.' 識別子 `=' 式2

式 1 を評価して得られるオブジェクトに対して、識別子= というメソッドを、式 2 を引数にして呼び出します。

例:

class C
    def foo
        @foo
    end
    def foo=( v )
        @foo = v
    end
end
c = C.new
c.foo = 5   # c.foo=( 5 ) のように変換される
p c.foo     # => 5

属性は Module#attr を使って同じように定義できます。

例:

class C
    attr :foo, true
end
c = C.new
c.foo = 5   # c.foo=( 5 ) のように変換される
p c.foo     # => 5

 いわゆるセッター(setter)のことか。以下の糖衣構文があるけど、自前で作るときは上記のようにするってことね。というかattrでも定義できたんかい。それは初めて知ったぞ。

  • attr_reader
  • attr_writer
  • attr or attr_accessor

 そういえば両方書いてあったときはどうなるんだ? 試してみたら自前実装したほうが優先された。定義順は問わない。

class E
  attr :foo
  def foo=(v)
    @foo = v + v
  end
end
e = E.new
e.foo = 3
p e.foo #=> 6

class F
  def foo=(v)
    @foo = v + v
  end
  attr :foo
end
f = F.new
f.foo = 3
p f.foo #=> 6

自己代入

例:

foo += 12       # foo = foo + 12
a ||= 1         # a が偽か未定義ならば1を代入。初期化時のイディオムの一種。

 やっぱり自己代入って+=とかのことだったのね。

 つまり+=などは「再定義できない演算子」ということか。マジで? それはクソですわ。試してみたところ+=@+=+=@すべてエラーになった。

class G
  def +=(v)
    p '+='
  end
end #=> syntax error, unexpected `end', expecting end-of-input
class G
  def @+=(v)  #=> `@' without identifiers is not allowed as an instance variable name
    p '+='
  end
end #=> syntax error, unexpected `end', expecting end-of-input
class G
  def +=@(v) #=> `@' without identifiers is not allowed as an instance variable name (SyntaxError)
    p '+='
  end
end #=> syntax error, unexpected `end', expecting end-of-input

文法:

1 op= 式2     # 式1は通常の代入の左辺のいずれか

op は以下のいずれかです。演算子と=の間にスペースを空けてはいけません。

+, -, *, /, %, **, &, |, ^, <<, >>, &&, ||

 つまり以下のとおりってことね。それくらいちゃんと書いて欲しいわ。

+= -= *= /= %= **= &= |= ^= <<= >>= &&= ||=

この形式の代入は

1 = 式1 op 式2

と評価されます。ただし、op が &&, || の場合には、

1 op (式1 = 式2)

と評価されます。この違いは属性参照のときに

obj.foo ||= true

が、

obj.foo = obj.foo || true

でなく

obj.foo || (obj.foo = true)

と呼ばれることを示します。(obj.foo= は obj.foo の結果によって呼ばれないかも知れません)

 まずは||=の説明からして欲しかった。||=は左辺が未定義なら右辺値を代入する演算子である。

 ローカル変数は未定義のそれを参照するとエラーになる。だがインスタンス変数は未定義のそれを参照するとnilが返る。そしてnilは真偽値でいうとfalseを返す。その特性を利用することで、インスタンス変数の初期値代入として||=が使われる。

class H
  def initialize
    @name ||= 'ytyaru'
    @name ||= 'YTYARU'
    p @name
  end
end
H.new #=> 'ytyaru'

 初回代入について。@nameは最初、未定義なので値はnilである。@name ||= 'ytyaru'self.name || (self.name = 'ytyaru')と評価される。つまりnil || (self.name = 'ytyaru')である。代入式は代入した値を返すのでnil || 'ytyaru'である。||は左から順に真である値を返す。Rubynilfalse以外すべて真である。||の最初の値はnilでありfalseなのでそれは返さず、次の値ytyaruをみる。これはnilでもfalseでもないため真なので、この値を返す。よって初回代入@name ||= 'ytyaru'@name'ytyaru'が代入される。

name = 'ytyaru' #=> 'ytyaru'
> 1 || 2   #=> 1
> nil || 2 #=> 2

 2回目代入について。@nameには'ytyaru'が代入されている。その値はnilでもfalseでもないため真である。よって||=による右辺値の代入は行われない。式に展開すると@name = 'ytyaru' || 'YTYARU'と同じである。このとき@name'ytyaruは同値なので冗長な表現である。あくまで説明のために書いた式にすぎない。

 ||=Rubyのようにクラスを再定義できてしまうからこそ有意義なのだろう。既存値があればそれを優先する。なければやむなく代入する。防波堤のような役割の代入なのだろう。

 さらに定数への代入にも効果的。ふつうなら2回目以降の代入では警告される。しかし||=をもちいれば2回目以降はそもそも代入されないため、警告も出ない。

MAX ||= 100
MAX ||= 200
p MAX #=> 100

 ただ、もし2回目以降の値を想定しているなら、エラーになるべきかもしれない。

多重代入

例:

foo, bar, baz = 1, 2, 3
foo, = list()
foo, *rest = list2()

文法:

式 [`,' [式 `,' ... ] [`*' [式]]] = 式 [, 式 ... ][`*' 式]
`*' [式] = 式 [, 式 ... ][`*' 式]

多重代入は複数の式または配列から同時に代入を行います。左辺の各式はそれぞれ代入可能でなければなりません。右辺の式が一つしか与えられなかった場合、式を評価した値は配列に変換されて、各要素が左辺のそれぞれの式に代入されます。左辺の要素の数よりも配列の要素の数の方が多い場合には、余った要素は無視されます。配列の要素が足りない場合には対応する要素の無い左辺には nil が代入されます。

 C#Pythonでいうタプルと思われる。でも数が合わなくてもよいのは柔軟性が高い。いや高すぎる。紛らわしいコードが書けてしまう。

左辺の最後の式の直前に * がついていると、対応する左辺のない余った要素が配列として代入されます。余った要素が無い時には空の配列が代入されます。

 左辺*の多重代入は配列展開? いや右辺の配列化か?

例:

foo, bar = [1, 2]       # foo = 1; bar = 2
foo, bar = 1, 2         # foo = 1; bar = 2
foo, bar = 1            # foo = 1; bar = nil

foo, bar, baz = 1, 2    # foo = 1; bar = 2; baz = nil
foo, bar = 1, 2, 3      # foo = 1; bar = 2
foo      = 1, 2, 3      # foo = [1, 2, 3]
*foo     = 1, 2, 3      # foo = [1, 2, 3]
foo,*bar = 1, 2, 3      # foo = 1; bar = [2, 3]

多重代入は括弧により、ネストした配列の要素を代入することもできます。

(foo, bar), baz = [1, 2], 3       # foo = 1; bar = 2; baz = 3

 このほうが見やすい、か?

特殊な形式の代入式も多重代入にすることができます。

class C
  def foo=( v )
    @foo = v
  end
  def []=(i,v)
    @bar = ["a", "b", "c"]
    @bar[i] = v
  end
end

obj = C.new
obj.foo, obj[2] = 1, 2     # @foo = 1; @bar = ["a", "b", 2]

 結局、ただ2つ目が捨てられているだけでは?

左辺が ,' で終る場合や、*' の直後の式を省略した場合にも余った要素は無視されます。

例:

foo,*    = 1, 2, 3      # foo = 1
foo,     = 1, 2, 3      # foo = 1
*        = 1, 2, 3

特に最後の単体の `*' はメソッド呼び出しにおいて引数を完全に無視したいときに使用できます。(メソッド呼び出しの引数の受け渡しは多重代入とほぼ同じルールが適用されます)

例:

def foo(*)
end
foo(1,2,3)

 わけわからん。ふつうに引数定義しなければいいじゃん。以下のように。わざわざ引数を受け付けるようにしつつ無視させる意義ってあるの?

def foo
end

多重代入の値は配列に変換された右辺です。

 なにこの説明。

範囲式

例:

1 .. 20
/first/  ...  /second/
(1..)

文法:

1 `..' 式2
式1 ` ... ' 式2
式1 `..'1 ` ... '

条件式以外の場所では式1から式2までの範囲オブジェクトを返します。範囲オブジェクトはRangeクラスのインスタンスです。 ... で生成された範囲オブジェクトは終端を含みません。

 コードで書くと以下は同じってことね。

1 .. 3
Range.new(1,3)

終端を省略した Range は終端のない範囲を表現できます。 (1..) は (1..nil) の構文糖です。 (1...) とは exclude_end が違うため、オブジェクトとしては等しくありませんが、ary[1..] と ary[1...] のように通常の使い方では同じように扱えます。

 コードで書くと以下は同じってことね。

1 .. 
Range.new(1,nil)

 ary[1..]というのはPythonでいうスライスのことかな? 1番目以降すべての部分配列を返すって意味だと思われるが。なんの説明もない。試してみると、合っているっぽい。

a = [0,1,2,3,4]
p a[2..] #=> [2,3,4]

when 1.. のように書くと、行継続とみなされるため、終端なし Range にするにはかっこが必要になることがあるので、注意してください。

 when 1..というのはcase-when式のことかな? コード例がないからわからん。試してみる。

 範囲オブジェクト1..を括弧で囲わないと行継続になってしまい構文エラーになる。

case 2
when 1..
  p 'A' #=> syntax error, unexpected string literal, expecting `do' or '{' or '(' (SyntaxError)
when 4
  p 'B'
else
  p 'C'
end

 1..を括弧で囲むとRangeオブジェクトになる。

case 2
when (1..)
  p 'A'
when 4
  p 'B'
else
  p 'C'
end
=> 'A'

条件式としての範囲式

条件式として範囲式が用いられた場合には、状態を持つ sedawk 由来の特殊な条件式として振る舞います。フリップフロップ (flip-flop) とも呼ばれます。

「..」の場合:

  1. 初期状態では式1だけを評価し、式1が真を返すまでは false を返します。
  2. 式1が真を返すと true を返します。式2が真なら初期状態に戻ります。
  3. この後は式2だけを評価し、式2が真を返すまで true を返します。
  4. 式2が真を返すと true を返したあと、初期状態に戻ります。

「...」の場合:

  1. 初期状態では式1だけを評価し、式1が真を返すまでは false を返します。
  2. 式1が真を返すと true を返します。
  3. この後は式2だけを評価し、式2が真を返すまで true を返します。
  4. 式2が真を返すと true を返したあと、初期状態に戻ります。

例:

# よくある使いかた

5.times{|n|
  if (n==2)..(n==3)
    p n
  end
}
#=> 2
    3

5.times{|n|
  if (n==2)...(n==3)
    p n
  end
}
#=> 2
    3

# 「..」と「...」の違いを示すためだけの例

5.times{|n|
  if (n==2)..(n==2)
    p n
  end
}
#=> 2

5.times{|n|
  if (n==2)...(n==2)
    p n
  end
}
#=> 2
    3
    4

 C言語では以下のように書くけど上記のようにも書けるってことね。

5.times{|n|
  if 2 <= n && n <= 3
    p n
  end
}

 if修飾子を使うともっと短くかける。

5.times{|n| p n if (n==2)..(n==3)}

 case-when式には修飾子がない。なのでend;を省略できない。処理を先頭に書けない。

5.times{|n| case n; when 2..3; p n end}

 case-in式だと網羅的に書かねばならないためelse句を省略できない。

5.times{|n| case n; in 2..3; p n; else; end}

 短く書きたいから条件式としての範囲式は(n==2)..(n==3)のような記法があるのだろう。ただ、この書き方もどうかと思う。

 Pythonだったら2<= n <=3のように略記できたのに。それをRubyでやると構文エラーになる。残念。

n = 2
p 'A' if 2 <= n <= 3
undefined method `<=' for true:TrueClass (NoMethodError)

 この記法がベスト。短くて明瞭だと思うのだが。Rubyでは使えない。

and

例:

test && set
test and set

文法:

`&&' 式
`and'

左辺を評価し、結果が偽であった場合はその値(つまり nil か false) を返します。左辺の評価結果が真であった場合には右辺を評価しその結果を返します。 and は同じ働きをする優先順位の低い演算子です。

p(nil && false) # => nil
p(false && nil) # => false
p(1 && 2) # => 2

and を伴う式をメソッドの引数に渡す場合は二重に括弧が必要となります。

p(true && false)    #=> false
p((true and false)) #=> false

 &&andの両方使えるのはすばらしい。演算子は記号のほうが読みやすいので助かる。Pythonandしか使えない。アルファベットまみれになって値なのか演算子なのかパッと見わからない。

or

例:

demo || die
demo or die

文法:

`||' 式
式 or 式

左辺を評価し、結果が真であった場合にはその値を返します。左辺の評価結果が偽であった場合には右辺を評価しその評価結果を返します。 or は同じ働きをする優先順位の低い演算子です。

p(1 || 2) # => 1
p(nil || false) # => false
p(false || nil) # => nil

or を伴う式をメソッドの引数に渡す場合は二重に括弧が必要となります。

p(false || true)    #=> true
p((false or true)) #=> true

 ||orの両方使えるのはすばらしい。&&(and)とおなじで読みやすく書ける。

not

例:

! me
not me
i != you

文法:

`!' 式
not 式

式の値が真である時偽を、偽である時真を返します。

`!=' 式

!(式 == 式)と同じ。

`!~' 式

!(式 =~ 式)と同じ。

 =~ってなに? ググってみたら正規表現演算子らしい。そういえばbashにもあったな。

コード例 意味
/xxx/ =~ yyy 正規表現のメソッド =~正規表現と文字列をマッチさせる。両辺を入れ替えても機能します。
/xxx/ !~ yyy 正規表現のメソッド =~ の否定。マッチが失敗したらtrueを返します。

not を伴う式をメソッドの引数に渡す場合は二重に括弧が必要となります。

p(! false)      #=> true
p((not false))  #=> true

 !notの両方使えるのは嬉しい。&&||とおなじ。

条件演算子

 C言語でいう三項演算子のこと。

例:

obj == 1 ? foo : bar

文法:

1 ? 式2 : 式3

式1の結果によって式2または式3を返します。

if1 then2 else3 end

とまったく同じです。

 この書式の三項演算子はいいね。Pythonは妙ちきりんな順序なので。

対象環境

$ uname -a
Linux raspberrypi 5.10.52-v7l+ #1441 SMP Tue Aug 3 18:11:56 BST 2021 armv7l GNU/Linux