見出し画像

Retro-gaming and so on

RE: プログラミング学習日記 2024/02/13〜

龍虎氏の記事(2024/02/17)に対するコメント。

そう言えばクラスって何だろう?

クラス、とは何度か指摘してるが、事実上ほぼ「構造体」の事だ。

クラス≒構造体

Pythonには構造体がない。その代わりにクラスがあるんだな、って考えて入口は間違ってない。
以前別の記事でも書いたけど、CやPascalのプログラマはC++/C#やJava、あるいはObject Pascal/Delphiの「クラス」に対して混乱する事はまずない。
と言うのも彼らにとっては、クラスってのは「拡張された/便利な構造体/レコード型」以上の意味を持たないから、だ。
だからプログラミング・ニュービーとは違って、「クラスを使う」って事に対して躊躇しない。原則彼らには慣れ親しんだモノ、だからだ。
龍虎氏の場合、既にRacketの構造体(struct)やOCamlの構造体に慣れ親しんでるだろうから、そういう意味だと敷居は低い筈、なんだ。

Defでクラス内の関数であるメソッドが定義されるので、Initは必ず書かねばならない初期化メソッドってことだな?引数で持ってきた値をインスタンス変数なるものに束縛すると

defと言うキーワードに騙されないようにしよう。
事実上、Racketの構造体で

(struct Circle (radius))


(struct Animal (name))

と書くトコをPythonでは

class Circle(object):
 def __init__(self, radius):
  self.radius = radius


class Animal(object):
 def __init__(self, name):
  self.name = name

と書くだけ、なんだ。
見た目の違いに惑わされないように。実はこの2つ(RacketとPython)ではやってる事(Pythonの記述の方が長いにせよ)は全く同じ「作業」を表している。
ここをまずは突破する事。クラスは基本、構造体なんだ、って事をアタマに叩き込もう。


同様にイテレータを扱うためには必須のメソッドってことだな?

イテレータに関しては後で追加説明をするけど、Pythonの「イテレータの作り方」に付いては様式が決まりきっている。
このページを参考にする事。
例えばそのページでの「イテレータの作り方」を見れば、見様見真似でイテレータなんかはすぐ作れてしまう。
実際問題「イテレータの作り方」は様式が決まってるんで、クラスの知識が無くても書けちゃうくらい、なんだ。

見れば分かるけど、「あれだけリスト内包表記やunfoldrで悩んだ」素数列や素因数分解なんかも簡単にイテレータとして実装出来てしまう。

>>> list(prime_factors(20100))
[2, 2, 3, 5, 5, 67]

さて、ではイテレータの話を改めて書こうと思うけど、その前に。
実は龍虎氏はラッキーだ。「全く余計な一般的なノイズが耳に入らないお陰で」殆ど最初のプログラミング言語としてLispを学べた。
そういう意味だと苫米地氏に感謝だ(笑)。
Lispは高抽象度だ。
いや、最高抽象度だ。
だから「Lispの観点で」他の言語を見下ろす事が出来る。何度も繰り返すけど、ほげ言語のパラドックスが書いてる通りなんだよ。
上から目線、ってのは何とも嫌らしい表現だけど(笑)、一方これは利点がある。龍虎氏が「Lispより抽象度が低いプログラミング言語を学ぶ際」、殆ど前提知識が無くてもその言語を「使える」強力な立場を与えてくれるんだ。それはLispが高機能だから、だ。90%以上の確率で「あ、これはLispで学んだ✗✗だな」とアタリが付けられる。よってその機能を「すぐ使える」と言った龍虎氏自身の「知識の実用性」の裏付けが取れる、って事なんだ(尤も、その言語に付いてるその「機能」がLispに比べて使いづらい、って事は良くある・笑)。
これは僕自身の経験上でもそう、なんだ。「△△言語で✗✗がやりたい」となった時に、その機能を探してすぐ適用しやすい。これは他言語を学んだり使う際にも非常に強力な「アイディア」をLispで学んだお陰で、かつそれを適用出来るから、に他ならない。
反面、これもいっつも言ってるけど、「C言語はプログラミングの基礎」なんてバカな事を言って、Cの後、別言語を学ぼう、なんてしても、Java以外だとその言語の30%も使いこなせないだろう。C言語を学ぶとCの機能「以外は全部」難読に見えるから、だ。それはCが極端に抽象度が低いせいだ。結果、「高抽象度の言語はそれより低抽象度の言語での書き方を全て受け入れちまうの法則」により、あたかも「Cがプログラミングの基礎」と言う誤解を加速するだけ、の結果になる。事実は全く違うんだけど、「Cの書き方が通用した!やっぱCは基礎なんだ!」と言うアンポンタンを産むだけの結果になっている。
それくらい「高抽象度」の言語と「低抽象度の」言語の教育効果は違うんだ。

繰り返すけど、最初にLispを学んでおけば、「他の(Lispより)抽象度が低い」プログラミング言語なんつーのは90%くらいは使いこなせてしまうだろう。もちろん、全ライブラリを遺憾なく使いこなせて、と言う意味じゃなくって、そもそもの「Lispを学んだ事によるプログラミングと言うコンセプト」を遺憾無く発揮してドキュメントを読めば大体のトコは「分かる」って言ってる(要は、「勘所を押さえる事が出来る」)わけなんだが・・・・・・。
一方、確かに他言語に「Lispに無い機能」とか「Lispに無いコンセプト」が無いわけじゃない。
そしてその一つが、Pythonのイテレータ、なんだ。

ここでもう一回用語の定義をする。
イテレータは一般に「反復子」等と訳されるが、どれが代表的なイテレータなのか、と言うと意見の一致を見ないだろう。
ただ言えるのは、「人間は反復が嫌いだ」と言う事だ(笑)。どんなにプログラミングに慣れてもまず必ず、って言っていいほど「書くのを間違える」(笑)。エラーが一番多く出るのは反復に関連したトコだろう。
言い換えると、イテレータと言う「概念」は、「如何に反復を簡単に記述出来るか」と言った研究の成果だ。とにかく間違える「反復記述」を如何に簡単に書けるようにするのか。
言い換えると、言語デザイナはそこに力を注ぐわけなんだが。
例えばLisp(Scheme/Racket)で有名なイテレータはご存知、mapfoldだ。これも直接反復するのを避ける為にある。カテゴリは確かに「関数」なんだけど、その存在理由は「反復を簡単に記述する為」だ。
Lispなんかの方針では、「イテレータ設計」に於いても汎用性を重視する。要は抽象度を上げるわけだよな。「色んなデータ形式に対して汎用に使えるモノを」関数として提供しようとする。これはこれで一つの解で、ある意味「汎用化」と言うのがLispでの「設計」のキーワードなんだ。
一方、昨今のモダンなオブジェクト指向は、真逆の考え方を良く取る、んだ。Lispと逆に、「あるデータ型に特化した繰り返し機構を作れないか?」と。そう、このコンセプトの「違い」を把握しておかないとならない。
繰り返す。昨今のモダンなオブジェクト指向言語は、「汎用のイテレータ」作成よりも「あるデータ型に特化した」反復機構を用意した方がエエんちゃう?と言うアイディアを採用するケースが多い。もっとハッキリ言うと、「データ型自体に繰り返し機能を付けちゃえ、そしたら間違えないだろ」って言う方針を取る、んだ。
この辺が決定的にLispには無いアイディアなんだ。
Pythonと言うプログラミング言語は、言語上、「反復構文」ってのは実はwhileしかないんだ。じゃあforって何だ?と言うと、これはデータに付随してるイテレータを「よ〜いドン!」させる為のキーワードで、for自体が「ループを回してる」わけじゃないんだよ。
重要な事なんで繰り返す。Pythonや競合するモダンなオブジェクト指向言語だと、「データ自体が」繰り返し機能を持つようにデザインされてる事が多い。汎用の繰り返し構文の数を増やすより、「データ自体が自ら勝手に繰り返す」ような設計がされてる言語の方が増えてるんだ。

もう一度「クラス」と言うアイディアに戻ろう。クラスは原理的には構造体だ。もうちょっと言うと「ユーザー定義型」だ。従って、作成したunfoldrや、上の「ある数以下の素数を全部出す」プログラムやら「与えられた数を素因数分解するプログラム」は使い方を一見すると「関数」って思っちゃうけど、そうじゃなくってunfoldrと言う「データ型」であり、素数を作り出す「データ型」であり、素因数分解の結果を出す「データ型」なんだ。
いや、データ型、って言うと何か「不変の、とても計算なんて行わない静的なモノ」って印象になるかもしんない。でも、良く考えてみると、Lispなんかでは関数でさえデータ型だった。だから「クラス」と言うユーザー定義型が「計算」しても何もおかしくないんだ。
それより、Pythonで言う「イテレータ」と言うのは、クラスにデフォルトで付随する色んな機能の中で、「反復を強調して」設計された、ビルトインのデータ型、もしくはユーザー定義型の事、なんだ。

ちとおさらいしてみる。例えばlst = [1, 2, 3, 4, 5]と言うリストを作って、

for i in lst:
 print(i)

とすると、1、2、3、4、5が印字される。
しかし、forがこの「繰り返し」をしてるわけじゃなくって、forがやってるのはリスト型、と言うデータ型に「持ってるイテレータを起動しろ」って言うだけ、なんだ。繰り返し自体を行ってるのは、Lispじゃ考えられないが、リストの方、なんだ。
そしてこれで分かるのは、リスト型もイテレータが組み込まれている。
これはある意味、Lispから見ると衝撃的なパラダイムだろう。

しかし、一つ疑問に思うかもしんない。Pythonはかなり上手く設計されてるけど、じゃあ、「データ型自体が繰り返しを持つ」のなら、構文がデータ型によって「違う」って事はあり得ないのか、と。データ型を設計して毎回毎回「イテレート機能を付け足す」ような事をしてたら、「繰り返し」の構文が増えるばっかにならんのか、と。
いや、この不安はその通りなんだ。繰り返すけど、Pythonはかなり上手に設計されてるんで、「どのデータ型でも共通の構文でイテレータを起動する事が出来る」ようになってる。
一方、例えばRubyなんかは、Pythonに比べると、良く言えば「豊富なイテレータ用構文」が準備されている。悪く言えば「繰り返し構文あり過ぎじゃね?」となってる(笑)。
まぁ、この辺は「言語設計者の言語設計上の趣味」ではあるんだけどね。Rubyの場合は恐らく、「コンテクストによって形式が違うのは当たり前だ」って思想なんだろう。見た目が違えば意味が違ってくる筈だ、と。
一方、Pythonの方は「なるたけシンプルに」が思想で、これも何度も繰り返すが、Haskellからリスト内包表記を持ってきたのが結果大きかった、と思う。この存在のお陰で、「色んな形式でイテレータを起動せなアカン」ってのを無くす事に成功してるんだ。
ちなみに、Lispの観点から言うと、「えええええ?」って言うような記述を取る言語も存在する。JavaScriptだ。
例えばLisp(Scheme/Racket)で

; '(1 2 3) の各要素を2乗したリストを返す。
(map (lambda (x) (* x x)) '(1 2 3))
⇒ (1 4 9)

と書くような事をJavaScriptでは

js> [1, 2, 3].map((x) => x ** 2)
[1, 4, 9]

と書く(笑)。「リストにmapを適用する」んじゃなくって「リストに付随してるmapイテレータを起動する」ようになってんだな(笑)。僕も最初見た時「ええッ?」って驚いたわ(笑)。
まぁ、JavaScriptはオブジェクト指向言語ではあるけれども、Pythonみたいなクラス、ってシステムを採用はしていない。ただし、データにイテレータが付いてる、って辺りはPythonと同じだ。
Python、Ruby、JavaScriptのようなモダンなオブジェクト指向言語の場合、「繰り返し機能」は、プログラミング言語の構文より「データ型の方に付いてたり」するんだ。そういうLispとはまた違った「イテレータと言うアプローチ」をこの際、把握しておこう。
この辺はさすがにLispじゃ学べない、んだ。

 ふーむ・・Self.seedを破壊的変更してるということか?

うん、ごめん(笑)。してます(笑)。
反省してる(笑)。

メソッドはインスタンス内をスコープ出来るということかな?

あー、うん、ちょっと意味が分からんが、メソッドの第一引数selfは、「その(クラスで定義された)データ型そのもの」を意味してるんで、そういう意味だと「そのメソッドのスコープ内にある」って事になる。


 なんとなく分かってきたけどメソッドの定義を聞く。

まずメソッドに付いては以下の記事を参照。

「メソッド」の他に言語によっては「メンバ関数」等と呼んだりする。
しかし、いずれにせよ、本質的な「意味」ってのは「名前空間内で同一の関数名を使いたいんだけど、衝突させたくない」って事なんだ。
例えば次のようなクラス2つを考える。

class Tawashin(object):
 def __init__(self):
  pass
 def oppai(self):
  print("おっぱいもみもみ")


class Cametan(object):
 def __init__(self):
  pass
 def oppai(self):
  print("おっぱいふみふみ")

Tawashinと言う民族は女のおっぱいを見ると「もみもみ」しようとする。
一方、Cametanと言う民族はおっぱいを見ると「ふみふみ」しようとする。
うむ、何ともくだらん例だ(笑)。
いずれにせよ、ここではTawashinと言う「ユーザー定義型」とCametanと言う「ユーザー定義型」が成立してんだけど、「女のおっぱいを見ると」行う反応は、両者ともoppaiで記述したい、って願望があるわけね。
もちろん、これを外部的に関数として、

def oppai(data):
 if isinstance(data, Tawashin):
  print("おっぱいもみもみ")
 elif isinstance(data, Cametan):
  print("おっぱいふみふみ")
 else:
  pass

って書くのも可能だけど、メンド臭いじゃない(笑)。もう一つ民族が増えたら関数oppaiを修正せなアカン。やってらんない。
そこで同名の関数をデータ型によって使い分けたい、って言った場合、「じゃあ、クラスに所属させりゃ良くね?」ってのが一つの解決策なわけ。それがクラスに所属する「メソッド」の正体だ。

>>> t = Tawashin()
>>> t.oppai()
おっぱいもみもみ
>>> c = Cametan()
>>> c.oppai()
おっぱいふみふみ

もう一回言うけど、これは「名前空間内での同一名関数の衝突を避ける為の」一つの方法論だ、って事なのね。実装の選択として「これが唯一無二の解」ってわけじゃない。わけじゃないんだけど、Pythonはこの方式を採用している。
一方、件の記事でも書いたけど、例えばANSI Common Lispはメソッドをクラスに所属させない。総称関数と言う方式で、その総称関数用テーブルにメソッドを所属させる。ANSI Common Lispのオブジェクト指向に影響を受けたGaucheの場合も同形式を採用してる、ってのもいつか見た通りだ。
いずれにせよ、繰り返すけど、本質的には「同じ名前空間内で、同名関数を使いたい」と言う事柄に対しての「解決策」が、実装はどうあれ「メソッド」、あるいは「メンバ関数」の意味なんだ。


 子クラスで内容を定義するってのなら親クラスに(わざわざPassって書くために)作る必要なくない?文法的に必要なのか?

いや、必要ないです。
ただ、オブジェクト指向言語でのコーディングの作法として、オブジェクト指向を「マジメ」にやる人ってのは、最初に抽象クラス、ってのを作る事が多いのね。

多分、Javaで広まったやり方なんだけど。
最初に抽象クラス、ってデータ型を設計して、「実際の使いたいデータ型」はその抽象クラスを継承して作る、と。
この例だと「動物」が抽象クラスで、具体的なデータ型を「動物を継承した」犬にしたい、と。で、抽象クラスでは「どんなメソッドが必要なのか」ってのを「使えなくても」定義しておくわけ。で、設計時に「特に何もせん」事をpassとして指定して、「継承した側で」具体的な動作で「オーバーライドする」って事を良くやるんだよ。
これは文法じゃない。あくまで「オブジェクト指向でのプログラミング作法」で、また絶対正しいやり方だ、ってわけでもないわけだ。
この辺は好みかなぁ・・・いや、Javaで「流行った」ってのは裏返すと「Javaでプログラミングを行ってる」会社で、この形式だと色んなトラブルが減ります、とかそういう話だと思うんだ。Javaは「会社向けの言語」だからな。
一方、個人でプログラムする際には、あんま関係ねぇと思ってる。
いやさ、この辺の「作法」に付いては僕も昔悩んだ事があるんだよ。例えば「ガンダム」って言うクラスを作る際に、先に「ロボット」って言うクラスを作るべきか否か、とか(笑)。でも、「ロボット」の前にじゃあ「人型」は要らんのか、と(笑)。
だってずーっと遡っていけば行けるだけ行けちゃうじゃん(笑)。上の例でも「動物」は「生物」を継承するべきなんじゃねぇの?とか(笑)。
んで、バカ臭くなって考えるの止めちゃったんだよな(笑)。
オブジェクト指向、ってそういう「理屈で突き詰めるとヘンな作法」ってのが山ほどあって、結局「とある会社内でのとある方針」以外に理論的、って言える指針が実はねぇんだよな(笑)。だから「考えるの止めた」わけ(笑)。


 ふーん、別にインスタンス変数必須でも無いのか

表面的な、機能的にはね。
当然、変数に「代入」する事には意味がある(ちなみに、「インスタンス変数」とは、クラス内で定義された変数を差し、代入されたdog自体はフツーに変数だ)。

>>> dog1 = Dog("ポチ")
>>> dog2 = Dog("ムギ")
>>> dog1.x = "hoge"
>>> dog2.x
Traceback (most recent call last):
File "<pyshell#175>", line 1, in <module>
dog2.x
AttributeError: 'Dog' object has no attribute 'x'
>>> dog1.x
'hoge'

同じクラスから作られた2つの変数だけど、dog1に何をしようとdog2は「全く影響を受けていない」事が分かるだろう。
言わば、dog1はdog1の「クロージャ」を持ってて、dog2はdog2の「クロージャ」を持ってる、と捉える事が出来る。
オブジェクト指向信者はこれがオブジェクト指向の「強み」だと力説する。互いに独立してて、一つの変更が他の同型のオブジェクトに影響を与えない、んだ。
また、これが「銀行口座によるオブジェクト指向の説明」が良く行われる理由だ。僕が銀行からお金をおろす際に、僕の「銀行口座」と言うオブジェクトはあくまで単体として存在し、僕が金をおろしたから、っつって龍虎氏の銀行口座にはまるっきり影響を与えない、と言う「プログラム上の保証」なわけだな。

例によって「オブジェクト指向」の仕組みの理論的な説明はSICPに書かれている
似た理論的解説は実用Common Lisp(PAIP)の13章にも書かれている。
適宜その辺のドキュメントも参照して欲しい。
また、このブログでも直近で「オブジェクト指向のラムダ式による説明」を書いている。

今回は以上、かな。
ハングマン頑張って(笑)。
とは言っても、Lispやってきたからハナクソだ、とか思ってるけど(笑)。
  • Xでシェアする
  • Facebookでシェアする
  • はてなブックマークに追加する
  • LINEでシェアする

最近の「RE: プログラミング学習日記」カテゴリーもっと見る

最近の記事
バックナンバー
人気記事