
どのように機能しますか?
ここでは、プログラムテンプレートからコンパイル段階を構築するための基本的なブリックを配置します。 関数型プログラミングに慣れていない人のために、必要な概念を簡単な言語で説明します。 基本的なことを検討したら、新しい言語、その構文、機能、および問題に進む準備ができています。 すぐに主要なものに直接行きたい人は、記事タイトルの再表示の横にある緑色の写真にスクロールできます。
正直なところ、数年前、私はここで、式の演算子の優先順位を変更するアルゴリズムの例を使用して、コンパイル時プログラムの原理を説明する投稿を書き込もうとしていました(つまり、 別の優先順位で「再構築」し、その方法を計算する しかしではない )
長い記事を写真とUMLダイアグラムでほぼ終えましたが、突然、メタプログラムがハブで頻繁に言われることに気付きました。 さらに、実際の使用のために
constexpr
を追加しましたが、私は倒錯したエンターテイメントの分野に留まり、最小限の言語機能を使用したかったのです。 注目すべき記事HurrTheDurrからのコンパイル時の生活とコンパイル時の 解釈、またはilammyからのC ++ 11のラムダの代替理解 (2つ目はよりハードコアで、
constexpr
using template
を
using template
ない)は、倒錯した心が知る必要があるほぼすべてを説明しました。 その結果、記事をドラフトコピーのまま残しましたが、メタプログラミングへの欲求は残しませんでした。 そして今日、私は新しく書かれたテキストと新しいアイデアで戻ってきています。
私の努力にもかかわらず、読者が素材を理解するのがまだ難しい場合は、これらの記事を読むことをお勧めします-その後、Paul Hudack et al。からのHaskellの簡単な紹介( パート1 、 パート2 )Denis Moskvin and Lambda calculus by ibessonovによる JavaScript 。 このすべてがある程度著者に影響を与え、おそらく読者が影響を受けるでしょうか?!
メタ値とメタ関数
クラステンプレートは、ある意味では、型を受け入れて返す型の関数です。 したがって、パターンはメタ関数になり、タイプと整数はメタ値になります。 たとえば、メタ関数
std::vector
はメタ値
T
を取り、メタ値
std::vector<T>
を返します。 メタ値には、一連のメタフィールド(C ++の観点から-ネストされたクラス、型エイリアス、静的定数フィールド)があり、それによって最も興味深いことができることが重要です。
value
などのメタフィールドを1つ選択し、メタ関数が返す値として理解することができます。 整数上のメタ関数は非常に簡単に書くことができます:
template <int x> struct square { static const int value = x * x; };
メタ関数は、FPで次のように
square<5>::value
。
しかし、整数は通常の値であり、単純なC ++で十分なので、整数をメタ式で使用するのはややスポーツマンらしくないでしょう。 真の正直なメタはタイプです。 それを作成する最も簡単な方法は、構造体を宣言することです:
struct One; struct Zero;
C ++の観点から見ると、
one
と
zero
宣言されて
zero
だけで、実際に
zero
できません。 これで十分ですか? はい それでは何が等しく、それらをどのように使用するのですか? 彼らは平等であり、それは私たち自身にとってのみ重要です。 これらは、シンボリック計算で使用できる抽象的なシンボルの一種です(Mathematicaやその他のものとほとんど同じです)。 メタプログラムは、さまざまな複雑さのメタ式を計算します。 最初にこれらの表現を検討し、少し後にシンボルの解釈と結果の表示を扱います。
ブール値として0と1の場合、NOT、AND、およびOR関数を記述するのは自然です。 否定のメタ機能を考慮してください。
template <typename T> struct Not { typedef Zero value; }; template <> struct Not <Zero> { typedef One value; };
値を取り
Not
。この値がゼロの場合は1を返します。 それ以外の場合はすべて、ゼロを返します。 したがって、テンプレートの特殊化により、初期段階ではパターンマッチング(サンプルとの比較)があります。特定の値を持つ1つ以上の引数に対する関数の動作を個別に記述することができます。必要な専門化。 これを使用して、すでに再帰的なものを書くことができます(たとえば、階乗、説明を
fac<0>
と
fac< >
に分割する)。
一覧
デフォルト
value
制限されていない
value
、多値関数がどのように見えるか想像できます。 関数型プログラミングの専門家になじみのある
Cons
リストコンストラクターと空の
Nil
リストを作成しましょう。
template <typename h, typename t> struct Cons { typedef h head; typedef t tail; }; struct Nil;
FPの
Cons
は、最初の要素のリスト(先頭)と他の要素のリスト(末尾)を作成する関数です。 通常リスト
Cons<one, Cons<two, Cons<three, Nil>>>
一致します。 リストを操作するには、そのコンポーネントを取得できる必要があるため、
Cons
、ヘッド(
Cons<...,...>::head
)とテール(
Cons<...,...>::tail
を返す多値関数にします
Cons<...,...>::tail
)。 OOP愛好家は、
Cons
がコンストラクターであり、
head
と
tail
がgetterであることを想像できます。 FIでは、すべての割り当ては、変更された引数を使用した関数
関数型言語にはループがないため、このようなリストは通常、再帰的に実行されます。
// template <typename list> struct negate { private: // typedef typename list::head h; // typedef typename Not<h>::value not_head; // typedef typename list::tail t; // typedef typename negate<t>::value not_tail; // public: // - , // typedef Cons<not_head, not_tail> value; }; // template <> struct negate <Nil> { // - typedef Nil value; };
Haskellはペイントほど怖くないことがわかりました。 どれも非常にシンプルに見えるので、この言語で例を明確にするために例を追加することは罪ではありません。 準備ができていない読者のために、C ++や学校の数学との主な違いに注意してください。Haskellでは、引数はスペースで区切られ、括弧でグループ化されません。 すなわち Haskellでは
f (x+y) (gy) z
として記述されます。
-- - ( - ), -- data List a = Cons a List | Nil -- head (Cons x _) = x -- tail (Cons _ xs) = xs -- -- negate (Cons x xs) = Cons (Not x) (negate xs) negate Nil = Nil
強く型付けされたHaskellやC ++とは異なり、アヒルの型付けはテンプレート言語で機能します。
negate<One>::value
はもちろん動作しませんが、
One
に
head
と
tail
メタフィールドがある場合は動作します。 ただし、:
negate<One>
::value
negate<One>
「参照解除」され
negate<One>
いない間、プログラムはコンパイルを続行
高階関数
関数型言語では、関数は同じ意味を持ちます。 したがって、高次関数を書くことができます-関数を引数として受け取るか、関数を返します。 たとえば、リストの要素ごとの変換はFWPマップを使用して実行されます。
-- map f (Cons x xs) = Cons (fx) (map f xs) -- map f Nil = Nil
STLには、
std::transform
と呼ばれるこのような変換が含まれています。 この言語では、
map
メタ関数は、テンプレートによるテンプレートパラメーター化を使用して宣言されます。
// // f - - template <template <typename> class f, typename list> struct map { private: typedef typename list::head h; // typedef typename f<h>::value new_head; // typedef typename list::tail t; // typedef typename map<f, t>::value new_tail; // public: // typedef Cons<new_head, new_tail> value; }; // template <template <typename> class f> struct map<f, Nil> { // typedef Nil value; };
f
については
f
ここで前に説明した
Not
関数に置き換えて、ネガのリストを計算できます。
typedef map<Not, Cons<One, Cons<One, Cons<Zero, Nil>>>>::value list; // list Cons<Zero, Cons<Zero, Cons<One, Nil>>>
式の命名操作
typedef
は代入演算子と同等であることが
typedef
。 または、関数型言語の方が正しいと思われるのは、名前と式の対応を指定する操作です。
C ++ 11以降では、型とテンプレートのエイリアスを使用して、既知の式を通じて値と関数を設定できます。
using x = Not<One>::value; template <typename xs> using g = Cons<x, xs>;
メタ関数が単一のメタ値を返す場合、テンプレートエイリアスは
value
メタフィールドを削除します。 これは、プログラマーが
::value
明示的に指定するのが面倒で、計算可能性の要件を課す場合により便利です。
プログラムは単純に無限再帰に入ることができます
コンパイルされますが、
はコンパイルされないことを思い出してください。 面倒なメタフィールドを使用して、条件に応じてブランチの1つだけの
を計算し、この値を返すブランチメタ関数を作成できます。 式の1つが評価されることはありません。すべての操作は
受け取ったときにのみ実行され、誰もそれに触れませんでした。
同時に、テンプレートエイリアスを持つバリアントは、一部の
が既に計算された
の意味を持つことを提供し
。 また、2つの再帰オプション間で分岐する場合、計算は無限になります。これは、コンパイル段階でのスタックオーバーフローに相当します。
negate<Zero>
コンパイルされますが、
negate<Zero>::value
はコンパイルされないことを思い出してください。 面倒なメタフィールドを使用して、条件に応じてブランチの1つだけの
::value
を計算し、この値を返すブランチメタ関数を作成できます。 式の1つが評価されることはありません。すべての操作は
::value
受け取ったときにのみ実行され、誰もそれに触れませんでした。
// template <typename expr1, typename expr2> struct If <True, expr1, expr2> { // expr1, expr2 , expr1::value expr2::value - typedef typename expr1::value value; } // If<One, id<One>, destroy<the_world>>::value
同時に、テンプレートエイリアスを持つバリアントは、一部の
g<x>
が既に計算された
f<x>::value
の意味を持つことを提供し
f<x>::value
。 また、2つの再帰オプション間で分岐する場合、計算は無限になります。これは、コンパイル段階でのスタックオーバーフローに相当します。
template <typename expr1, typename expr2> struct If <True, expr1, expr2> { // expr1, expr2 typedef expr1 value; }; // If<One, One, destroy<the_world>::value>::value
基本的に、テンプレートエイリアスはテンプレートに相当する単なる構文糖であり、省くことができます。
短絡
通常のC / C ++では、関数が完了した後に引数とローカル変数が死ぬ場合、関数型言語では、関数は親関数の引数とローカル変数に依存する関数を返すことができます。 したがって、親関数はすでに完了していますが、そのローカル変数は子関数に「アタッチ」されたまま残ります。 実際には、これは、たとえば単項関数などのインターフェイスが必要な場合に便利であり、その実装には依存するコンテキストもあります。
たとえば、
fx
は、関数
f
引数
x
暗黙的に使用して、単項関数
g
返します。
fx = g where g xs = Cons x xs -- : (fx) xs , , map (fx) list
Cons
を残すこともできますが、
g
は既に記述されている
map
関数に単項として渡すことができますが、
Cons
はバイナリで
Cons
ません!
ただし、通常のC ++はクロージャの欠如に悩まされません
これを行うには、機能オブジェクトまたはラムダ関数を使用します。 つまり、グローバル変数を介して関数に追加のパラメーターを渡すと、関数を使用する代わりにプログラムの柔軟性に違反する場合、オブジェクトが使用されます。
には必要なコンテキストがすべて含まれ、
はインターフェイスに必要な引数を取ります FPの美しいコンセプトは、同等のOOPコンセプトに置き換えられています。 標準
テンプレートも提供されています!
this
には必要なコンテキストがすべて含まれ、
operator ()
はインターフェイスに必要な引数を取ります FPの美しいコンセプトは、同等のOOPコンセプトに置き換えられています。 標準
std::function
テンプレートも提供されています!
// C++11: , struct f { f(something& x) : x(x) {} something_else operator () (something_else& xs) { // xs - return Cons(x, xs); } private: something x; // }; // C++11 : -, something x; auto f = [x](something_else& xs) { return Cons(x, xs); };
関数(たとえば、
int(int)
)を機能オブジェクト(たとえば、
std::function<int(int)>
)に置き換えると、C ++プログラマーは、クロージャーの本格的なアナログを取得します。
テンプレートレベルでは、「関数→オブジェクト」を置き換える必要はありません。 クロージャは言語自体によってサポートされています:
template <typename x> struct f { template <typename xs> struct g { typedef Cons<x, xs> value; }; }; // : f<x>::g<list>::value map<f<x>::g, list>
C ++やHaskellとは異なり、ここでは
f
文字は
g
「取り出し」ますが、残りは正直なクロージャーです。 式
f<x>::g
は、通常の単項関数(たとえば、
Not
)の代わりに使用できます。
リストの無制限の引数または構文糖
可変長テンプレートを使用すると、リストから関数を記録できます。 繰り返しますが、場合によってはより便利ですが、それらがなければ
Cons
と
Nil
と平和に暮らすこともできます。 さらに、これはさらに単純な動きになることがあります。 2つのリストをメタ関数に渡すには、2つのリストを渡すだけで十分です:
f<xs, ys>
、可変個のテンプレートの場合、最後のリストを除くすべてのリストをキャプチャするラッパーを指定する必要があります:
f<list_wrapper<x1,x2,x3>, y1, y2, y3>
。これは、任意の長さの引数のリストは1つである必要があり、行に記述されたいくつかのリストは単に「結合」するためです。 計算の最後に、ラッパーを返す必要があります。 C ++では、いくつかのタイプのリストを入力することはできません。 そして、ラッパーからリストを「取り出し」てメタ関数(たとえば
f
)に転送するには、メタコールバックを使用する必要があります。このメタ関数
f
を受け入れ、リストの内容を渡すメタメソッドをラッパーで実装します。
typename <typename... xs> struct list_wrapper { // list_wrapper<...>::call , // xs struct call<template <typename...> class f> { typedef typename f<xs...>::value value; }; };
反復/再帰リスト処理は、これを使用しても実装することはできません。 たとえば、
negate
を実装
negate
は、補助単項メタ関数
f
を作成する必要があります。これは、リストの末尾に再帰的に
negate
を適用した結果を受け取り、ヘッドの否定を計算し、リストのラッパーを返します。
typename <typename x, typename... xs> struct negate { template <typename... ys> struct f { typedef list_wrapper<Not<x>::value, ys> value; }; typedef typename negate<xs>::template call<f>::value; }; typename <> struct negate<> { typedef list_wrapper<> value; };
フォームの美しい記録のためにそれが判明しました 発表時に、これらのシーケンスをパック、転送、およびアンパックする必要がありました。
negate
は、
Cons
/
Nil
バージョンと比較しても、かさばります。 ここでは、FPの基本とHaskell→C ++の機械的交換で「通常の」バージョンを作成するのに十分な場合、より深刻な製造が必要になります。 そのため、可変テンプレートを使用して、パラメーターのシーケンスを
Cons
/
Nil
リストに変換するラッパーを作成し、プログラムを実装するときにそれを使用することをお勧めします。 そのため、快適なコンマ区切りリストを使用してリストを設定し、より単純なカテゴリーで考えることができます。
外の世界への出口
これまで、値と純粋な関数のみを考慮してきました(適用の結果は引数のみに完全に依存し、同じ引数に対しては関数は同じ値を与えます)。 プログラムの結果を出力するには、コンパイル段階で純粋な関数と計算の両方を使用する必要があります。
メタプログラムでの計算が完了すると、結果の式は実世界のエンティティとして解釈され、変数に保存されるか、画面に表示されます。 出力にテンプレートクラスを使用できます。 コンストラクターまたは他のメソッドには、命令コードが含まれています。 パターンマッチング機能がすべてのオプションを検討するのに十分な場合、メタ値自体(たとえば、
void One::print();
)内、またはメタ関数内のいずれかになります。 ここで、たとえば、
print<...>
インスタンスを構築する段階で、引数(単位、ゼロ、またはリスト)を出力する
print
メタ関数:
template <typename T> struct print { print () { std::cout << "unknown number" << std::endl; } }; template <> struct print <One> { print () { std::cout << "1" << std::endl; } }; template <> struct print <Zero> { print () { std::cout << "0" << std::endl; } }; // print<Not<Zero>::value>() "1"
同様に、リストや必要なものを表示できます。 たとえば、AND、NOT、XOR、バイナリ加算器を実装し、数字を
One
と
Zero
リストとして表現し、加算、乗算などを構築できます。
C ++ 11および
decltype
の登場以前は、型に対して純粋に機能的な計算を実行し、対応する型の変数およびC ++オブジェクトを作成し、それらに対して計算を実行してから、再び型に対する計算に戻ることは不可能でした。
sum<sum<One, two>, three> // - sum<sum<One, two>, three>() // - do_something(sum<One, two>(), three()) // - sum<sum<One, two>(), three()> // , // .. sum<decltype(sum<One, two>()), decltype(three())> // C++11;
もちろん、この機能により、より低コストでC ++でイデオロギー的に正確なコードを記述できましたが、テンプレートのメタ言語のイデオロギーはわずかに揺れました。 たとえば、Haskellでは、命令型世界の有毒カップを飲んだ関数は、それによって永遠に毒されたままであり、純粋な世界に属する通常の値を返す権利がありません。 つまり、機能的な純度から命令型への移行は、一方向にのみ行うことができます。
decltype
する
decltype
型は純度の類推であり、値は命令型の世界の本質でした。型のみがテンプレートパラメータになり、型は型変換によってのみ取得できました。 値を作成すると、型に戻ることは不可能でした。
C ++ 11では、型を計算し、この型の値を作成し、何らかの方法で変換し、
decltype
を使用して結果型を型の式に渡すことが
decltype
ます。 ただし、
decltype
は引数を
decltype
せず、機能の純度に違反しない「カウントを開始した場合の表現のタイプ」という質問にのみ答えます。 したがって、変数の式が
decltype
ブラケットを離れるまで、清潔さが維持されます。 テンプレート言語の観点から、
decltype
演算子のオーバーロードとともに使用する
decltype
有益です。 次の例では、式は同等ですが、下部はかさばっていません。
typedef sum<sum<one, two>, three> x; typedef decltype(one() + two() + three()) x;
かっこから出ると、清潔さに違反します。
auto v = one() + two() + three(); // v typedef decltype(v) x;
C ++テンプレートに変換する難解な言語
コンパイル時FJとしてのC ++テンプレートの主な問題は、過度の冗長性です。 これらすべての
::value
、角括弧、および
typename
は、プログラマを強く使い果たし、プログラムコードを拡張します。 もちろん、このスタイルでプログラミングすることを決めた人は苦しむべきだとロジックは言っていますが、...これはIT担当者の脳がしばしば傾いている異常なエンターテイメントの1つです。 倒錯が保存されることを確認したいと思いますが、苦しみがまだ耐えられる程度に。
一般に、私は独自の言語を作成し、C ++テンプレートに翻訳することにしました。 これを行うには多くの方法があります。 JSFuckトランスレーターによって行われたような最も独創的な変換を使用することもできます。 しかし、そのようなアプローチは、(a)別の不必要な言語を生成し、(b)C ++から離婚し、(c)まだ発明する必要があり、(d)実装にエネルギーを費やします。 そして、十分に強力で不必要なFYを開発して実装することは、面倒で役に立たないビジネスです。 特に、C ++テンプレートが既に関数、クロージャ、パターンマッチングに相当する構造を持っている場合...そしてそれはスポーツではありません。
私は最大の対応の道を選択しました。 私の言語のコードはシンプルに見えるはずですが、イデオロギー的にはC ++に似ています。 つまり、変換により、私の言語はC ++で最も逐語的に翻訳されたはずです。 これは、パターンを超える1キログラムの構文上の砂糖であると想定されていました。
上記のコードを作成した言語に書き直し、その機能と実際のアプリケーションを調べてみましょう。「ゼロ」と「1」の値の決定、数を否定する関数、リストのコンストラクター、リストを否定する関数の定義、FVPマップなどの決定から同じ順序で移動します。
値と関数
structキーワードは、値を宣言するために必須ではありません。
One; Zero;
関数宣言は、引数とその型を中括弧で囲み、「=」記号の後ろに本文を残します。特定のケースの説明は、一般的なケースの説明の後にありますが、引数の具体的な値はタイプを示していない点が異なります。
Not (val x) = Zero; // Not (Zero) = One; // And(val x, val y); // x,y != One,Zero And And(val x, One) = x; // One - , val x - And(val x, Zero) = Zero; // : Not(One) And(One, Zero)
C ++では、テンプレートパラメータはタイプ(
typename
)、別のテンプレート(
template <typename> class
)などにすることができ、これを指定する必要があります。したがって、meta-FVPを作成
typename
することはできません。また、タイプを示す要件は私の言語にも適用されます。デフォルトでは
val
、通常のメタ値に対応するタイプが設定されます(C ++の構造または通常のタイプ)。関数を記述するために、タイプを矢印(
->
)と組み合わせることができます。たとえば、
val -> val
-単項関数(1つのパラメーターを取るテンプレート)
(val, val) -> val
-バイナリー関数(2つのパラメーターを取るテンプレート)など。
ここで、翻訳のリテラル性から少し逸脱し、タイプエイリアスを導入しました。これは、C ++の(タイプエイリアス)とは異なります。を使用して
#type
同義語を設定して、レコードをもう少し短くし、適切な名前を付けて意味を明確にすることができます。
#type number = val; #type list = val; #type unary = number -> number; #type map_t = (unary, list) -> list; // map_t template <template <typename> class, typename> class
Cの
#type
類似物と見なすことができ
#define
、簡単な方法で手動で実行できるテキスト変換を定義します。
一覧
便利な機能として複数の返品をキャンセルしていません。これは、リストを実装するときに便利です。
Nil; Cons(val x, val y) { head = x; tail = y; }
C ++で翻訳された関数を引数に適用すると、自動的に展開され
::value
ます。それは
f(x)
同等
f<x>::value
です。しかし
Cons
、2つのメタポールのみ
head
が生成され
tail
ます。開示を防ぐために
::value
アポストロフィを使用して明示的に必要とされています
Cons(x, xs)'
。
私はこの問題について長い間考えていました。一方で
::value
は、頻繁に使用される構造の1つが自動的に開かれるべきでしたが、(a)開示されていない値を送信し(スポイラーの下で上記の分岐関数と無限再帰の問題を参照)、(b)以外のメタフィールドを使用する必要がありました
value
。その結果、私はピリオドを介してメタポールを書く「エスケープ」に落ち着き、逆演算子「!」を導入しました
::value
。
Cons(x, xs)'.head; // x Cons(x, xs)'.tail; // xs Not(Zero); // One Not(Zero)'!; // One
ただし、メタポール
::value
は複数のリターンと共存する可能性があります。
negate
C ++でリストの否定を実現し、返される可能性のあるローカルメタ変数を作成しました。ここ-同様に、すべての値のみがパブリックです:
// : // - , // negate (list list) = Cons(not_head, not_tail)' { h = list.head; // not_head = Not(h); // t = list.tail; // not_tail = negate(t); // } // ! // - negate (Nil) = Nil;
高階関数
単項関数とリストのタイプを宣言し、マップを作成したことを思い出してください。
// map (unary f, list list) = Cons(new_head, new_tail)' { h = list.head; // new_head = f(h); // t = list.tail; // new_tail = map(f, t); // } // map (unary f, Nil) = Nil;
もちろん、
unary
andの代わりに、それぞれを
list
すぐに示すこともできますが、レコードは長くなり、視覚的ではなくなります。
val -> val
val
短絡
クロージャーはC ++テンプレート自体によってサポートされているため、ここで異常なことはありません。
f(val x) = { g(val xs) = Cons(x, xs)'; } // : f(x)'.g(list) map(f(x)'.g, list)
ただし、返される関数の名前(gなど)を指定する必要があります。返される名前のない関数には、名前
value
とreturnが必要
value
です。C ++のクラスと同じ名前のメンバーが既にコンストラクターに与えられているため、新しい意味を与えることで
typedef
コンパイルエラーが発生します。
template <typename x> struct f { template <typename xs> struct value { // value! typedef Cons<x, xs> value; // value! }; };
C ++では、ネストされたテンプレートに特化できないため、子メタ関数のパターンマッチングの代わりに、他のメカニズムを使用する必要があります。
C ++では、テンプレート内でテンプレートパラメータ名を再利用することもできません。私の言語のこれらの名前はローカルメタ変数を参照し、テンプレートを超えないため、名前の変更を自動化しました。
f (val x) = g(Not(x)) { g (val x) = x; // g (x1) = x1 // .. x template f x template g }
私にとっては、このような変換は「スポーツマンらしさ」を追加しませんでした(手作業による些細な交換の可能性があるため)。
ある段階で、純粋に機能する部分が多かれ少なかれC ++に正常に変換され、少なくとも結果を出力できる必要があることが判明しました。この部分は、私にとって興味深いテンプレートのクリーンな世界の範囲を超えており(そのため、結果としてさらに悪いと考えられていました)、さらに重要なこととして、通常のC ++で十分に説明されました。テンプレートでコンパイル時コンピューティングを使用するプログラムでは、テンプレートのない通常のC ++が外部へのアクセスを担当します。つまり、自分の言語をC ++のスーパーセットにするか、C ++の独自のアナログを作成する必要がありました。もちろん、最初のオプションを選択しました。
残ったのは、C ++コードを挿入し、満足して手をこするためのタグを入力することでした。しかし、論理的な問題が発生しました:ソースコードでは
::value
、メタ関数を適用するために使用されることはどこにも記載されていません。
f(x)
-これ
f<x>::value
ではなく
call<f, x>::result
。この知識は翻訳者の内部に保存されており、プログラムでの使用は抽象化を打ち破ります。
f(val x) = One; f(Zero) = Zero; main { print<f<One>::value>(); // ? }
興味のない命令型部分に関するアイデアはほとんどありませんでした。いくつかのオプションを考え出した後、(a)通常のC ++を使用する可能性のある命令型ブロックを導入し、(b)純粋な関数世界から値をエクスポートすることにしました。
impure { }
値/関数を宣言する代わりにブロックが表示され、関数を宣言するときに「=」記号の直後に表示される場合があります。これらの場合、C ++コードはプログラムの適切な場所に挿入されます。式をエクスポートするには、キーワードをその前に配置し
impure
ます。C ++の観点から見ると、これは式で記述されたタイプのオブジェクトを作成することと同等です。
作業のデモンストレーションとして
impure
、リストの「プリンター」を準備します。
print(val list) { head = list.head; tail = list.tail; impure { // typedef- head, tail : print() { impure print(head); // "print<head>();" std::cout << ", "; impure print(tail); } } } print(Zero) = impure { // print(Zero) { impure { ..., print() { std::cout << "0"; } } print(One) = impure { print() { std::cout << "1"; } } print(Nil) = impure { print() { std::cout << std::endl; } }
名前空間
理論的には私の言語は、その中に、従来のCで使用されるライブラリ++、I「prokinul」を作成することができますので
namespace
、と
using namespace
:
namespace ns1 { namespace ns2 { f(val x) = x; } } namespace ns3 { using namespace ns1.ns2; g(val x) = f(x); }
using namespace
また、必要な措置。C ++では、場合によっては、ビューレコードでは十分ではありません
f<x>::y<z>
。あなたの場合
f
、
x
または
z
-ではない特定のタイプ/テンプレート、およびテンプレートパラメータには、
::y
ブラックボックスの出力となります。計算時に得られるものを示す必要があります
::y
-タイプまたはパターン(
typename f<x>::y
またはなど
f<x>::template y<z>
)。私はこれらの命令を自動化せず、より単純な手動記述のために構文糖を実装しなかったため、ドットを使用するたびに「typename」のように見えます。すなわち
f<x>::y<z>
は間違った種類のものに翻訳されます
typename typename f<x>::value::typename y<z>::value
。名前空間の場合、これは不要で
using namespace
あり、typename挿入の必要性を回避します。
ラムダス
ラムダ関数なしで関数型言語を残したくありませんでした。完全なサポートを実装することはできませんでしたが、代わりに、少なくとも関数を宣言するときに引数を「=」記号の反対側に転送することは可能です
f (val x) = y(x) { y(x) = g(x); } // f = lambda (val x) -> y(x) { y(x) = g(x); } //
ラムダの完全なサポートは、C ++では(a)匿名クラスを作成できず、(b)タイプと同等になるまで説明を展開せずにテンプレートに相当するものを宣言できないという事実によって妨げられます。つまり、数学的には、 あなたは書くことができますが、代わりに 使用する必要があります 。 命令型プログラミングのユーザーはすでにこの状況に慣れていますが、関数型プログラミングの標準ではこれはかなり面倒です。
template <typename x> struct f {}; // f using g = f; // // , template <typename x> using g = f<x>; // g<x> - , f<x> -
ビューエントリを実装
map(lambda(val x) -> y, xs)
するには、言語を変更し、一時的な名前でテンプレートを生成する必要があります。
前述のように、
value::value
これはコンストラクタであるため、ラムダを返すラムダを直接実装することはできません。ラムダから関数を返し、ビューレコードを許可
g = f
するには、他の概念を使用し、トランスレーターをほぼ完全に書き直す必要があります。
欠点と可能な解決策
- ローカル変数の非表示はありません。 コードに
挿入impure { private: }
するか、言語の基本的な変更を行うことにより、単純に解決されます。 - «typename».
, . (,fMap(fCurry(fCons).fApply(vXs), vObj.vYs)
—template
,typename
, «f», «v» «n» ).
— « ». - :
// x struct x { typedef internal::value type; // x }; // f(x) = x struct f { typedef internal::function type; // f template <typename x> struct value { typedef typename x::type type; // f struct result { // , value::value typedef x value; }; }; };
internal
;struct result
value::value
. - , ( , ), (typedef
), , . - : .
.
value
, . ,value::value
value1::value2
:
template <typename x> struct f { typedef internal::True uses_value1; // value1 template <typename y> struct value1 { typedef internal::False uses_value1; // value2 typedef Cons<x, y> value2; }; };
,uses_value1
,value1
value2
. - パターンパラメータとしての可変長パターンと整数は考慮されません。
実装のサポートに加えて、必要なval
追加の番号の種類(のint
、uint
、...)と配列([val]
、[int]
、[uint]
...)。メタフィールドを使用する場合、template
とtypename
で上記の問題を解決する必要があります。数値には何も必要ありません;型とパターンには必要です。
プログラム例
例としてリストを作成しましょう 、2つの方法で否定を取り、これらすべての値を以下に出力し
main
ます。
// : my_list = Cons(One, Cons(One, Cons(Zero, Nil)')')'; negated_list = negate(my_list); negated_list2 = map(Not, my_list); impure { int main() { // : std::cout << "my list is "; impure print(my_list); std::cout << "negated list is "; impure print(negated_list); std::cout << "negated list is also "; impure print(negated_list2); } }
プログラムは次を出力します。
my list is 1, 1, 0, negated list is 0, 0, 1, negated list is also 0, 0, 1,
プログラムのソースコード全体
impure { #include <iostream> } One; Zero; Not (val x) = Zero; // Not (Zero) = One; // And(val x, val y); // x,y != One,Zero And And(val x, One) = x; // One - , val x - And(val x, Zero) = Zero; #type number = val; #type list = val; #type unary = number -> number; #type map_t = (unary, list) -> list; Nil; Cons(val x, val y) { head = x; tail = y; } // : negate (list list) = Cons(not_head, not_tail)' { h = list.head; not_head = Not(h); t = list.tail; not_tail = negate(t); } // - negate (Nil) = Nil; // map (unary f, list list) = Cons(new_head, new_tail)' { h = list.head; new_head = f(h); t = list.tail; new_tail = map(f, t); } // map (unary f, Nil) = Nil; print(val list) { head = list.head; tail = list.tail; impure { print() { impure print(head); std::cout << ", "; impure print(tail); } } } print(Zero) = impure { print() { std::cout << "0"; } } print(One) = impure { print() { std::cout << "1"; } } print(Nil) = impure { print() { std::cout << std::endl; } } my_list = Cons(One, Cons(One, Cons(Zero, Nil)')')'; negated_list = negate(my_list); negated_list2 = map(Not, my_list); impure { int main() { std::cout << "my list is "; impure print(my_list); std::cout << "negated list is "; impure print(negated_list); std::cout << "negated list is also "; impure print(negated_list2); } }
C ++での翻訳後のコード
#include <iostream> struct One; struct Zero; template <typename x> struct Not { typedef Zero _value; }; template <> struct Not<Zero> { typedef One _value; }; template <typename x, typename y> struct And; template <typename x> struct And<x, One> { typedef x _value; }; template <typename x> struct And<x, Zero> { typedef Zero _value; }; struct Nil; template <typename x, typename y> struct Cons { typedef x head; typedef y tail; }; template <typename list> struct negate { typedef typename list::head h; typedef typename Not <h> ::_value not_head; typedef typename list::tail t; typedef typename negate <t> ::_value not_tail; typedef Cons <not_head, not_tail> _value; }; template <> struct negate<Nil> { typedef Nil _value; }; template <template <typename> class f, typename list> struct map { typedef typename list::head h; typedef typename f <h> ::_value new_head; typedef typename list::tail t; typedef typename map <f, t> ::_value new_tail; typedef Cons <new_head, new_tail> _value; }; template <template <typename> class f> struct map<f, Nil> { typedef Nil _value; }; template <typename list> struct print { typedef typename list::head head; typedef typename list::tail tail; print() { print <head> (); std::cout << ", "; print <tail> (); } }; template <> struct print<Zero> { print() { std::cout << "0"; } }; template <> struct print<One> { print() { std::cout << "1"; } }; template <> struct print<Nil> { print() { std::cout << std::endl; } }; typedef Cons <One, Cons <One, Cons <Zero, Nil> > > my_list; typedef typename negate <my_list> ::_value negated_list; typedef typename map <Not, my_list> ::_value negated_list2; int main() { std::cout << "my list is "; print <my_list> (); std::cout << "negated list is "; print <negated_list> (); std::cout << "negated list is also "; print <negated_list2> (); }
翻訳者
私はJavaScriptで翻訳者を書きました(この言語が大好きで、簡単に考えられます)パーサージェネレーターとしてPEG.jsを使用しました。
起動時に、トランスレーター(GitHub の言語ページを参照)は、コマンドラインパラメーターとして指定された名前のファイルを読み取り、結果のC ++プログラムテキストをstdoutに出力します。
node src/compile <> #
翻訳者が多かれ少なかれ獲得し、作業プログラムが書かれるとすぐに、私はこのすべてをGitHubに投稿し、ジョークの数学者のように、「解決策があります!」と叫び、私はプロジェクトの開発にほとんど興味を失いました。私は上記の問題をタイプ/ハンガリーの表記法を交互に示すことで解決しようとしましたが、これには深刻な変化と繰り返される精神的ストレスが必要でした。考えて、準備ができて機能しているものを持っているのは怠でした。さらに、最大の適合性の原則に違反する可能性があります。追加のラッパーとそれらを使用するためのコードは、放送の単純さの美しさを殺し、テンプレートのネストの制限を超えてプログラムを近づけます。
したがって、私は栄誉にかかっており、誰かが興味を持ち、おそらく言語の開発に貢献してくれたら嬉しいです。