kuina / Kuin

Kuin Programming Language
264 stars 18 forks source link

「**」でオーバーライドしたメソッド内で、親のメソッドの戻り値が取得できるようにする #73

Closed kuina closed 6 years ago

kuina commented 6 years ago

「**」でオーバーライドしたメソッド内で、親のメソッドの戻り値が取得できるようにします。 superという予約語の変数を用意し、そこに戻り値が格納される仕組みです。

kuina commented 6 years ago

meの代わりにsuperという変数で親メソッドの呼び出しができないかという提案も戴きました。

検討したところ、やっぱり実装上の都合から難しそうでした。 Kuinでは、meや他のインスタンスが型情報を持っているため、 型情報を親のものに書き換えたsuperインスタンスを別途生成することになるからです。

ただメソッド呼び出しの際に型情報を親のものに読み替えるような演算子にすることは可能です。 その場合、演算子を仮に「.*」とすると、

do me.f() {自身のメソッドが呼び出される}
do me.*f() {親のメソッドが呼び出される}

のようになります。

しかし懸念は2点あります。

(1)演算子にするとどこでも親のメソッドが呼び出せるため、 me以外のインスタンスに対して

var hoge: Piyo :: #Piyo
do hoge.*f() {Piyoの継承元クラスのメソッドが呼び出される}

のように書けてしまいます。(検出して弾くのは困難。) 別に書けても悪くはありませんので、「普通はmeに対して使う」と説明した上で割り切って提供するのはアリです。

(2)オーバーライド元のメソッドではなく、必ず親のメソッド呼び出しになるので、 親の親からオーバーライドしていた場合はオーバーライド元が呼び出せません。 オーバーライド元のメソッドをたどるようにすることもできなくはないですが、 パフォーマンスが落ちるため微妙です。

以上を考えると、おとなしく「**」の仕組みを続けるほうが良さそうに感じましたがいかがでしょうか。

tatt61880 commented 6 years ago

すみません、理解を深めるために質問させてください。 実行時のパフォーマンスは落ちると思いますが、「子クラスのインスタンスを親クラスの型にキャストすることで、親クラスのメソッドを呼び出す」というのではダメなのでしょうか。

func main()
    class C1()
        +func f()
            do cui@print("C1.f\n")
        end func
    end class
    class C2(C1)
        +*func f()
            do cui@print("C2.f\n")
        end func
    end class
    var c2: C2 :: #C2
    do (c2 $ C1).f()
    if((c2 $ C1) =$ C1)
        do cui@print("(c2 $ C1) =$ C1\n")
    end if
    if((c2 $ C1) =$ C2)
        do cui@print("(c2 $ C1) =$ C2\n")
    end if
end func

上記のコードを実行すると、予想に反して

C2.f
(c2 $ C1) =$ C1
(c2 $ C1) =$ C2

と表示されたので、現状の上記のコードではアップキャストが機能していないようですが…。

ちなみに、上記の方針でタイトルに書かれている「オーバーライドしたメソッド内で、親のメソッドの戻り値が取得できるようにする」という件を実現する場合は下記のような書き方になると思います。(現状はスタックオーバーフローします。)

func main()
    class C1()
        +func f(): int
            ret 1
        end func
    end class
    class C2(C1)
        +*func f(): int
            var x: int :: (me $ C1).f()
            ret x + 2
        end func
    end class
    var c2: C2 :: #C2
    do cui@print("c2.f() = \{c2.f()}\n")
end func
kuina commented 6 years ago

実行時のパフォーマンスは落ちると思いますが、「子クラスのインスタンスを親クラスの型にキャストすることで、親クラスのメソッドを呼び出す」というのではダメなのでしょうか。

Kuinでは親クラスの型にキャストしても、オーバーライドしたメソッドは子のものが呼び出されます。 これは、C++やC#でいうvirtualがすべてのメソッドにデフォルトで付いていると捉えてください。

普通クラスの継承は、子のほうでさまざまな特殊処理をオーバーライドで実装しておき、 それを親クラスとしてまとめて意識せずに呼び出せる使い方がされますが、 親クラスにアップキャストしたときに親側のメソッドが呼ばれるようになると、この使い方ができなくなります。 C++やC#でも、virtualをつけないオーバーライドはあまりしないと思っていました。(そんなことはないですかね…?)

kuina commented 6 years ago

それはさておき、Kuinの内部実装的な話になると、 インスタンスをアップキャストしても、インスタンス自体(型情報含む)には何も変更を行わないことで 子クラスのメソッドが常に呼ばれるようになっています。

これを親クラスのメソッドが呼ばれるようにアップキャストする演算子を用意することはできますが、 その場合インスタンスの型情報を親クラスのものに書き換えてインスタンスをディープコピーするような処理を行う必要があります。 (型情報だけ書き換えると、実際のメモリ上のメンバの数などが食い違っておかしくなります。) 単にオーバーライド元を呼び出したいだけなのにディープコピーをさせるのはやっぱり抵抗がありますね…。

satonayu commented 6 years ago

私も、派生した子クラスを親クラスの配列で管理し、親クラスにキャストされた状態のまま子クラスでオーバーライドしたメソッドを呼び出す。という使い方をしています。 c系の言語を使ったこと無いからかもしれませんが、親クラスのメソッドにアクセスするために親クラスにキャストするっていうのはちょっと違和感を感じました。

tatt61880 commented 6 years ago

なるほど。解説ありがとうございます。 さとちーさんのご意見も理解の補助になりました。ありがとうございます。

ただメソッド呼び出しの際に型情報を親のものに読み替えるような演算子にすることは可能です。 その場合、演算子を仮に「.」とすると、 do me.f() {自身のメソッドが呼び出される} do me.f() {親のメソッドが呼び出される} のようになります。

上記に関して。

懸念の1つ目に関しては、 クラス内にsuperというキーワードを書けるようにして、内部的にme.と同等の処理に置き換えるという実装は可能ではないでしょうか? 「`.`」という文字列をパースするわけではないため、meと同様にクラス外で使用できないようにできると思います。

懸念の2つ目に関しては、 現在実装を検討されているsuperという特殊変数でも同様ではないのでしょうか?

kuina commented 6 years ago

クラス内にsuperというキーワードを書けるようにして、内部的にme.*と同等の処理に置き換えるという実装は可能ではないでしょうか?

既存の仕組みから大きく拡張する必要がありそうですが、これは可能かもしれません。 「.*」で挙げていた懸念点(特にオーバーライド元ではなく親クラスの呼び出しに限られる問題)があることや、 既存の仕組みから拡張することで、特殊なケースへの対応(superを変数として参照するとどうなるか等)がいろいろと発生しそうではあります。

現在実装を検討されているsuperという特殊変数でも同様ではないのでしょうか?

実は「**」の内部実装としては、意味解析の段階でオーバーライド元の関数を特定し(仮にFとします)、 自動で「do F()」というコードを追加することで実現していました。 ですので、superという特殊変数で対応する場合は、 「var super: 略 :: F()」というコードを追加することで簡単に実現できる見込みです。(やや裏技的ですが)

kuina commented 6 years ago

やっぱり me.super() でオーバーライド元メソッドが呼び出せるようにできないか、再度検討してみましょうか。 普通に

var super: func<略> :: F

でできそうな気がしてきたので…。

satonayu commented 6 years ago

Twitterで唸っててすみません…! 個人的にはとてもありがたいです。

実は「**」の内部実装としては、意味解析の段階でオーバーライド元の関数を特定し(仮にFとします)、 自動で「do F()」というコードを追加することで実現していました。

このF()のアドレスを内部的にfunc型の変数superに保存しておいて自由に使えるよって感じでしょうか。 自動呼び出しではなくなるので、これまでの仕様と変わってしまうのが申し訳ないです…!

kuina commented 6 years ago

このF()のアドレスを内部的にfunc型の変数superに保存しておいて自由に使えるよって感じでしょうか。

そうなります、これまでと比べると、ユーザがいちいち引数を書いて呼び出す必要があるという煩雑さは生まれますが 自由度は上がるため、可能かどうか試してみますっ。

kuina commented 6 years ago

ところで、v.2018.2.17 では、「**」の廃止のついでにいくつか気になっている構文を修正したいと考えています。

特に一番下の項目が大きな変更になるので、みなさんの意見を聞きたいです。 2進数や8進数リテラルの使用頻度はほぼ無いと思ったので、現状煩雑な16#~を#~にしたいのですが、 まだユーザが多くない今、思い切って変えても大丈夫でしょうか。

kuina commented 6 years ago

すみません、よく考えたら #~ はインスタンス生成演算子としてすでに使われてしまっていましたね。 やっぱり 16# のままにしておきます。忘れてください><

kuina commented 6 years ago

(かなり昔のKuinでは、16進数リテラルもインスタンス生成演算子も #~ にしていた気がするのですが、なぜ破綻しなかったのでしょう。)

ghost commented 6 years ago

16進数リテラルをC言語等と同じように「0x~」と書きたいです。いかがでしょうか

kuina commented 6 years ago

C系の言語では、 16進数… 0x~ 8進数… 0~ ですが、8進数が割と混乱の元なので、うっかり10進数と間違って書いてしまわないように 避けたい思いはありました。 「0~」は10進数の扱いで、「0x~」を16進数とする案は、混乱の余地はありますがアリかもしれません。

C系以外の言語では、必ずしも「0x~」というわけではなく、 Pascal系では「$~」、BASIC系では「&H~」でした。

Kuinは、文法的に使える記号から考えて、 16進数で色を扱うときによく使われる「#~」を採用していましたが、 任意のN進数表記に対応するために、「N#~」としました。

現状の「N#~」の仕様の妥当性は、 10進数・16進数以外で書くケースがどのくらいあるかによりますね。

tatt61880 commented 6 years ago

0¥d+を8進数リテラルではなく10進数リテラル扱いすると確かに混乱する可能性があり受け入れられにくいかもしれません。「C言語などで書かれたコードをコピペして持ってきた際にバグる可能性がある」といった指摘が想定できるため、そうするよりはコンパイルエラーにする方が良いと思います。

個人的には、 0x で16進数 0o で8進数 0b で2進数 は受け入れられやすいと思います。 JavaScriptでは、古い仕様では0〜だった8進数リテラルが廃止され、しばらくして0oが採用されたようです。 Pythonでは、Python 3.xで0oが採用され、0¥d+は廃止されたようです。

C++標準ではC++11まで書けなかった2進数リテラルがC++14で書けるようになったので、2進数リテラルに関しては需要はあると思います。 https://cpprefjp.github.io/lang/cpp14/binary_literals.html

GCCやClangの言語拡張として古くからサポートされていたほか、Java、Python、Dといった言語でも同じ構文でサポートされていた。 こういった経緯から、C++標準で2進数リテラルをサポートすることとなった。

と書かれています(C++14から対応)。

kuina commented 6 years ago

なるほど 0x、0o、0b にしましょうか。 ただちょっとまだ迷う部分がありますので、考えてみます。

narumincho commented 6 years ago

16進数は0x 2進数は0b 10進数は0始まり禁止 8進数リテラルはなし が良いと思います

ghost commented 6 years ago

8進数は書くケースがほとんどなく、C#ではサポートされていないので8進数は廃止でも問題ないと思います

kuina commented 6 years ago

そうしましょうかね。 ちなみに現状、16進数のa~fと、指数表記の「e」およびbit型の「b」が重複しているせいで 16進数が小文字で書けないようになっているのですが、 指数表記はfloatのみで16進数では書けないため回避できるとして、 bit型は「16#FFFFb8」のように「b」を使うのではなく、「0xffff8」のように無関係な記号を使いたいと思っています。 「」が良いのかどうかは検討の余地があります。

ghost commented 6 years ago

C#では、「0xFFFFFFFF」のように「」が数値区切りで使われています Kuinでもこのように書けたら便利だと思います

bit型は、「0xFFFF#16」のように「#」はどうでしょうか

(8bitで0xFFFFはオーバーフローしてしまいます…)

kuina commented 6 years ago

うーむ、考えたら考えるほど、現状の仕様と書き方が変わっただけで便利さはあまり変わらないように思えてきます。 「_」を数値区切りに使えるのは一見便利に思えますが、実際使うのかと言われると、なくてもさほど困らない気もしました。 「#」という案は、10進数リテラルに使ったときに 16#8 (16という数のbit8型リテラル) となり、これまでの表記と混同しそうです。

これまでの表記から変えるべき強い動機があるかどうかですね。

narumincho commented 6 years ago

16進数リテラルは今まで通り大文字だけで良いと思います 0xFFFFb16 _はなくてもいいと思います。

kuina commented 6 years ago

ふむふむ。

kuina commented 6 years ago

superの件ですが、一度挑戦してうまくいかなかったので後回しにしていました。 さとちーちゃんからリマインドがあったので、真面目に再考してみます。

Kuinでは、インスタンスが持つ型情報を安易に書き換えると、元の型に存在していたメンバ変数が置いてけぼりになって管理できなくなりますので、 型をむりやり親クラスにキャストするという方法ではうまくいきませんでした。

残る方法としては、メソッド呼び出しのときに型を指定させる案ですかね。 例1: piyo.[parentType]f() 例2: piyo.*f()

例2は親の親の親…があったときに piyo.****f() とさせるのかという疑問が残りますが、処理的には速いです。 .**** は許可せず、常に直近の親のみを許可するというのも手です。 例1は parentType にキャストできるかどうかをチェックする必要があり、若干処理負荷はありますが、可読性が良くなります。

迷いますが、ひとまずは例2案でやってみましょうか。

kuina commented 6 years ago

piyo.*f() で親のメソッドを参照するようにしたのですが、 インスタンスの型から親メソッドを辿る方法はダメですね。 一体どうすれば…。

kuina commented 6 years ago

superを、trueやinfのような定数の扱いにする方法も 値(アドレス)を確定する仕組みが複雑になるため断念しました。

オーバーライドしようとした関数の先頭で「var super」を宣言する方法を試します。

kuina commented 6 years ago

手元で動きました。 +** は廃止して、super() に置き換えることにします。

kuina commented 6 years ago

+** を廃止して、super() に置き換えました。 また、16進数リテラルを 16#~ ではなく 0x~ に変更しました。

super は、第一引数に「me」を追加して呼び出してください。 例えば

+*func f(str: []char)
    do super(me, str)
end func

のようになります。

このIssueで予定していた機能は実装しましたのでクローズします。