組込型にメソッドチェーンっぽいものを繋げてみるその2(D, C#, VB.Net, JavaScript, Python3 + Clojure, Ruby)


これの続き。

概要

前回で組み込み型にメソッドチェーンを組み込むことができることを確認した。
そこで今回はどこまで何をチェーンできるのか試してみた。
また、グローバルを汚染することになるjsに対し対応策を考えてみた。

実装要件

下記のモジュールを実装し、

10
.buy()
.buy()
.puts()
.gets("> ")
.insertStr("echo> ")
.puts()
.gets()

のように処理を行う。

ExtendObject モジュール

int object.put()

インスタンスの内容を改行無しで標準出力する。
戻り値は0を返す。

int object.puts()

インスタンスの内容を改行有りで標準出力する。
戻り値は0を返す。

string string.insertStr(string)

Stringインスタンスにの手前に文字列を結合して返す。

double number.buy()

Numberインスタンスの値を2倍にして返す。

string int.gets(string)
Promise<string> int.gets(string)

標準入力を返す。
インスタンスの値は使用しない。
JavaScriptのみ戻り値の方はPromise型とする。

Promise<void> object.gets(string)

プログラムを終了させる。
JavaScriptのみ実装。

実装

D言語

Program.d
import ExtendObject;
mixin ExtendObject;

void main(){
    10
    .buy
    .buy
    .puts
    .gets("> ")
    .insertStr("echo> ")
    .puts
    .gets;
}
ExtendObject
module ExtendObject;

template ExtendObject(){
    import std.stdio;
    template put(T){
        int put(T self){
            write(self);
            return 0;
        }
    }

    template puts(T){
        int puts(T self){
            writeln(self);
            return 0;
        }
    }

    string insertStr(string self,string s){
        return s~self;
    }

    template buy(T){
        T buy(T self){
            return self*2;
        }
    }

    string gets(int self){
        return readln();
    }

    string gets(int self,string s){
        write(s);
        return readln();
    }
}

名前空間代わりにtemplate使っていること以外は至って順当な実装。

C#

Program.cs
using ExtendObject;

static class Program{
    static void Main(){
        10
        .buy()
        .buy()
        .puts()
        .gets("> ")
        .insertStr("echo> ")
        .puts()
        .gets();
    }
}
ExtendObject
using System;
using static System.Console;

namespace ExtendObject{
    static class ObjectChain{
        static public int put(this object self){
            Write(self);
            return 0;
        }

        static public int puts(this object self){
            WriteLine(self);
            return 0;
        }

        static public string insertStr(this string self,string s){
            return s+self;
        }

        static public double buy(this object self){
            return Convert.ToDouble(self)*2;
        }

        static public string gets(this object self){
            return ReadLine();
        }

        static public string gets(this object self,string s){
            Write(s);
            return ReadLine();
        }
    }
}

数値型をObject型としていい加減に扱っていること以外は普通。前回からそのまま拡張。

VB.Net

Program
Option Infer On
Option Strict On
Imports project.ExtendObject

Module Program
    Sub Main
        Call 10 _
        .buy _
        .buy _
        .puts _
        .gets("> ") _
        .insertStr("echo> ") _
        .puts _
        .gets
    End Sub
End Module
ExtendObject
Option Infer On
Option Strict On
Imports System.Console
Imports System.Runtime.CompilerServices

NameSpace ExtendObject
    Module ObjectChain
        <Extension()>
        Function put(self As Object) As Integer
            Write(self)
            Return 0
        End Function

        <Extension()>
        Function puts(self As Object) As Integer
            WriteLine(self)
            Return 0
        End Function

        <Extension()>
        Function insertStr(self As String,s As String) As String
            Return s & self
        End Function

        <Extension()>
        Function buy(self As Object) As Double
            Return CDbl(self)*2
        End Function

        <Extension()>
        Function gets(self As Integer) As String
            Return ReadLine
        End Function

        <Extension()>
        Function gets(self As Integer,myPrompt As String) As String
            Write(myPrompt)
            Return ReadLine
        End Function
    End Module
end NameSpace

プログラムについては大体C#と一緒。
しかし、数字から書き始めると行数がどうとか怒られたのはびっくり。
あとはビルドする環境(MSBuild, .Net Core他)次第でモジュールのImports名が変わるらしいので注意。

Imports ExportObject

だったり、

Imports project.ExportObject

だったりする。
ちなみにこの時の"projectはプロジェクト(.vbploj)名(ファイル名?)を指す。

JavaScript(Node.js)

Program.js
"use strict";
const ExtendObject=require("./ExtendObject");

new class Program{
    constructor(){
        ExtendObject.using(async()=>{
            await 10.
            .buy()
            .buy()
            .puts()
            .gets("> ").then(v=>v
            .insertStr("echo> ")
            .puts()
            .gets()).then(v=>v
            .exit());
        });
    }
};
ExtendObject
"use strict";

module.exports=(()=>{
    const rl=require("readline").createInterface(process.stdin,process.stdout);
    const util=require("util");

    const ObjectChain={
        put:[Object,function(){
            process.stdout.write(util.format(this.valueOf()));
            return 0;
        }],

        puts:[Object,function(){
            console.log(this.valueOf());
            return 0;
        }],

        insertStr:[String,function(s){
            return s+this.valueOf();
        }],

        buy:[Number,function(){
            return this.valueOf()*2;
        }],

        gets:[Number,(myPrompt="")=>new Promise(resolve=>{
            rl.setPrompt(myPrompt);
            rl.prompt();
            rl.once("line",resolve);
        })],

        exit:[Object,()=>{
            process.exit();
            return Promise.resolve();
        }]
    };

    return class{
        static open(){
            for(var v in ObjectChain){
                if(ObjectChain[v][0].prototype[v]!=null){
                    throw new RangeError("Property already exists.");
                }
                ObjectChain[v][0].prototype[v]=ObjectChain[v][1];
            }
        }

        static close(){
            for(var v in ObjectChain){
                delete ObjectChain[v][0].prototype[v];
            }
        }

        static using(fn){
            if(typeof fn=="function"){
                this.open();
                const fnCall=fn();
                if(toString.call(fnCall)=="[object Promise]"){
                    return fnCall.then(()=>
                        this.close()
                    );
                }
                else this.close();
            }
            else throw new TypeError("Type requires function.");
        }
    };
})();

前回から最も手を加えたのがこの実装。
結局、グローバルオブジェクトの汚染は防げないものの、
拡張メソッドの存在確認と処理後のメソッド破棄で安全にしたつもり。
usingメソッドにコールバック関数投げ込むかopen・closeメソッドの間で使用可能。
getsメソッドの戻り値はNode.jsのため致し方なくPromiseを返却。

Python3

Program.py
from ExtendObject import *

class Program:
    @staticmethod
    def main():
        (
            f_(10)
            .buy()
            .buy()
            .puts()
            .gets("> ")
            .insertStr("echo> ")
            .puts()
            .gets()
        )

if __name__=="__main__":
    Program.main()
ExtendObject.py
class o_:
    def put(self):
        print(self,end="")
        return i_(0)

    def puts(self):
        print(self)
        return i_(0)

class s_(str,o_):
    def insertStr(self,s):
        return s_(s+self)

class n_(o_):
    def buy(self):
        return f_(self*2)

class f_(float,n_):
    pass

class i_(int,n_):
    def gets(self,myPrompt=None):
        return s_(
            input() 
        if myPrompt is None else 
            input(myPrompt)
        )

前回同様にプリミティブ型を継承した拡張型を使用。
値を返す時は必ず拡張型のコンストラクタを使用することだけ注意した。
ここからは余り関係ないことだけど、setup.pyでプロジェクトを作成したとき、

from ExtendObject import *

じゃなくて、

from .ExtendObject import *

としなきゃいけなかったのはなかなかハマってしまった。

以下、おまけ。

Ruby

class ExtendObject
    private
    module ObjectChain
        def put
            print self
            return 0
        end

        def _puts
            puts self
            return 0
        end
    end

    module StringChain
        def insertStr(s)
            return s+self
        end
    end

    module NumericChain
        def buy
            return self*2
        end
    end

    module IntegerChain
        def _gets(myPrompt="")
            print myPrompt
            return gets
        end
    end

    def self.exclude(obj,mod)
        mod.instance_methods(false).each{|v| 
            obj.class_eval{undef_method v}
        }
    end

    public
    def self.open
        Object.include ObjectChain
        String.include StringChain
        Numeric.include NumericChain
        Integer.include IntegerChain
    end

    def self.close
        exclude Object,ObjectChain
        exclude String,StringChain
        exclude Numeric,NumericChain
        exclude Integer,IntegerChain
    end

    def self.using(fn)
        open
        fn.call
        close
    end
end

Class.new{
    def initialize
        ExtendObject.using lambda{
            10
            .buy
            .buy
            ._puts
            ._gets("> ")
            .insertStr("echo> ")
            ._puts
            ._gets
        }
    end
}.new()

クラスベースかプロトタイプベースかの違いはあるけどアプローチはJavaScriptと一緒。
それゆえにJavaScriptと同様の懸念がありそう。

追記:

コメントに@raccyさんよりご意見頂きました。
Rubyにはrefineという機能があり、これを使うことで安全にオブジェクトの拡張ができるそうです。

Clojure

(defn put[self]
    (print self)
    0
)

(defn puts[self]
    (println self)
    0
)

(defn insertStr[self,s]
    (str s self)
)

(defn buy[self]
    (* self 2)
)

(defn gets
    ([self]
        (read-line)
    )
    ([self s]
        (print s)
        (read-line)
    )
)

((fn[]
    (->
        10
        (buy)
        (buy)
        (puts)
        (gets "> ")
        (insertStr "echo> ")
        (puts)
        (gets)
    )
))

Lisp系言語でチェーンはどうするのかなぁと思っていたけどこんな解決策があるんだね。
面白い。こういう機能がデフォルトで組み込まれているのもCommonLispやSchemeに比べて新しい言語ならではって感じ。

以上、ここまで。