3つのF#パラダイム

はじめに



.NETプログラミングに何らかの形で関係している人なら誰でも、Visual Studioの次のバージョンでは新しいプログラミング言語が既に組み込まれていることを知っています。 F#が単なるFNPをはるかに超えていることを示すために(FNPだけがたくさんありますが)、次のすべてを書きました。

この記事は、かなりの長さにも関わらず、言語の完全な機能を完全に説明することを意図していません。 これは簡単な概要であり、さまざまな機能を実証するために設計されており、それぞれが個別の記事に値するものであり、1つの記事にも値しません。

さらに、このような長い記事を書いたので、将来のために予備を作りたいと思ったので、将来、私は基本的なレベルの取るに足らないものに気を取られないでしょう。 もちろん、すぐに池に向かう-これは効果的ですが、どんな基盤も傷つけません。

そして、次回は、通常のプロフェッショナルプログラミングアクティビティに対するF#の適合性に関する刺激的なトピックの例を紹介します。

繰り返しになりますが、実際には大量のテキストがあります。 そして、私はあなたに警告しなかったと言ってはいけません。 =)



F#機能的



もちろん、まず第一に、F#は関数型言語です。つまり、F#で最も完全に実装されているのは、関数型パラダイムのサポートです。 ご存じのように、その中の多くのキーワードとリテラルはOCamlから借用されていますが、F#の主な作成者であるDon SymeがかつてOCamlに手を携えていたので、驚くことではありません。

F#についての多くの知識は、純粋な関数型プログラミング言語であり、読者は私の過去の投稿から学んだかもしれませんが、言語の完全な印象を作成するためだけに、すべてを簡単に繰り返します。



識別子、キーワード、機能。



奇妙なことに、F#を使用すると、プログラマは識別子を定義して、後で関数にアクセスできます。 これは、 letキーワードを使用して実行れ、その後に識別子の名前、パラメーターのリスト、等号(関数を定義する式)が続きます。 このようなもの:

let k = 3.14

let square x = x**2.0







命令型プログラミングとは異なり、最初の式は変数ではなく定数を定義します。その値はプログラムの実行中に変更できないためです。 一般的に、F#は関数と値を区別しません。関数はパラメーターとしても自由に渡すことができる値です。

すべてのF#キーワードのリストはここにあります 。 参考として提供されている2番目のリストの単語は、現時点では使用されていませんが、将来のために予約されています。 これらは使用できますが、コンパイラは警告を出します。

F#はカリー化された関数をサポートしますが、すべてのパラメーターを一度に転送できるわけではありません。

let add ab = a + b //'a -> 'a -> 'a

let addFour = add 4 //'a -> 'a







2番目の識別子は、1つの空きパラメーターから既に関数を定義し、もう1つは4として定義されています。これは、関数が値であるという理論をもう一度示しています。 関数は値であり、パラメーターの完全なセットを受け取っていないため、単に値でもある別の関数を返すだけです。

ただし、.NETのすべての関数にはカレー機能のプロパティはありません。また、Fで使用するために、タプル(複数の異種値のセット)が使用されます。 タプルはそれ自体に多くの異なるパラメーターを含むことができますが、F#は1つのパラメーターと見なされ、結果として全体にのみ適用されます。 タプルは、カンマで区切られた括弧内に記述されます。



let add (a,b) = a + b

let addFour = add 4







F#によると、不適切な型のパラメーター、つまり 'a *'の代わりにintに関数を適用しようとしているため、このようなコードはコンパイルされません。

ただし、独自の関数、特に他のプログラマが使用する関数を開発する場合は、使用する柔軟性が明らかに高いため、可能であればカレーにする必要があることに注意してください。

読者の皆さんは、F#関数では戻り値を明示的に定義する必要がないことにすでに気付いています。 ただし、関数内で中間値を計算する方法は明確ではありませんか? ここでF#はメソッドを使用しますが、その多くは、スペースの助けを借りて忘れることができたと思います。 関数の内部計算は通常、4つのスペースで区切られています。

let midValue ab =

let dif = b - a

let mid = dif / 2

mid + a







ちなみに、F#プログラムを見た人の1人がコード内に#lightコマンドが常に存在することに驚いた場合、その効果の1つは、スペースが重要になることです。 これにより、 in 、;;、 beginendなど、OCamlに由来する多くのキーワードと文字の使用が回避さます。

各識別子には独自の適用分野があり、定義の場所から始まり(つまり、定義の場所よりもコードでそれを使用することは不可能です)、定義されたセクションの終わりで終わります。 たとえば、前の例の中間識別子difおよびmidは、midValue関数の外側では機能しません。

関数内で定義された識別子は、外部レベルで定義された識別子と比較していくつかの特異性があります-letという単語を使用して再定義できます。 これは、中間値を保持するためのすべての新しい、ほとんどの場合ほとんど意味のない名前を発明することができないので便利です。 たとえば、前の例では、次のように書くことができます。

let midValue ab =

let k = b - a

let k = k / 2

k + a







さらに、これは完全な意味でのオーバーライドであり、変数の値の変更ではないため、識別子の値だけでなく、その型も非常に適切に変更できます。

let changingType () =

let k = 1

let k = "string"







F#では、マップ、リスト、フォールドなどのシーケンスを処理するバッチ機能により、ほとんどの場合、サイクルなしで実行できますが、必要な場合は、再帰を使用できます。 理解しやすいのは、サイクルまたは再帰は一般に未解決の問題であり、私の意見ではどちらもかなり実行可能です。 F#の関数がその定義内で自身にアクセスするためには、 letの後にrecキーワードを追加する必要があります。



F#は強く型付けされた言語です。つまり、間違った型の値を持つ関数を使用することはできません。 関数は、他の値と同様に、独自のタイプを持っています。 多くの場合、F#自体が関数のタイプを推測し、たとえば次のようにあいまいになる場合があります。

let square x = x*x







型は ' a-> ' aです。ここで、 'aはint、float、一般的にはanyで、*演算子がオーバーロードされます。

必要に応じて、関数パラメーターの型を自分で設定できます(たとえば、クラスメソッドを使用する必要がある場合)。

let parent (x:XmlNode) = x.ParentNode









ラムダと演算子



F#は匿名関数またはラムダをサポートします。これらは、関数がパラメーターとして別の関数に渡されるときに名前を付ける必要がない場合に使用されます。 以下のラムダの例:

List.map (fun x -> x**2) [1..10]







この関数は、1から10までのすべての数値の二乗で構成されるリストを作成します。

さらに、F#には、 functionキーワードを使用してラムダを定義する別の方法があります。 この方法で定義されたラムダには、パターンマッチング操作が含まれている場合がありますが、必要なパラメーターは1つだけです。 ただし、この場合でも、関数のカレー機能を保存することは可能です。

function x -> function y -> x + y







F#のラムダはクロージャをサポートしますが、これについてはレビューの後半でさらに詳しく説明します。

F#では、演算子(単項および二項)は、関数を呼び出すより美的な方法と見なすことができます。 C#と同様に、演算子はオーバーロードされるため、さまざまな型で使用できますが、C#とは異なり、演算子をさまざまな型のオペランドに適用することはできません。つまり、数字(および実数を持つ整数)の文字列を追加することはできません。キャストを行います。

F#を使用すると、演算子をオーバーロードしたり、独自に定義したりできます。

let (+) ab = a - b

printfn "%d" (1 + 1) // "0"







演算子には、次の文字の任意のシーケンスを使用できます!$%&* + _。/ <=>?@ ^ |〜:

let (+:*) ab = (a + b) * a * b

printfn "%d" (1 +:* 2) // "6"









リストの初期化



別の強力なF#テクニックは、リストを初期化することです。これにより、特別な構文を使用して、かなり複雑なリスト、配列、およびシーケンス(IEnumerableに相当)を直接作成できます。 リストは括弧[]で、シーケンスは{}で、配列は[| |]。

最も簡単な方法は、ギャップを決定することです。これは、たとえば次のように(..)を使用して設定されます。

let lst = [1 .. 10]

let seq = { 'a' .. 'z' }







また、もう1つ(..)を追加することで、間隔の選択ステップを設定できます。

let lst = [1 .. 2 .. 10] // [1, 3, 5, 7, 9]







さらに、リストを作成するときに、ループを使用できます(ループは、単一または任意の程度にネストできます)

let lst = [for i in 1..10 -> i*i] // [1, 4, 9,..]







ただし、これだけではありません。 リストを初期化するとき、 yieldステートメント (シーケンスに1つの要素を追加)とyield!を使用して、入力する要素を明示的に指定できます (多くの要素を追加します)、テンプレートとの論理構造、ループ、比較も使用できます。 たとえば、これは、このフォルダーとそのすべてのサブフォルダーに含まれるすべてのファイルの名前のシーケンスを作成する方法です。

let rec xamlFiles dir filter =

seq { yield! Directory.GetFiles(dir, filter)

for subdir in Directory.GetDirectories(dir) do yield! xamlFiles subdir filter}









パターン比較



テンプレートとの比較は、通常の条件ステートメントまたはスイッチに少し似ていますが、より多くの機能があります。 一般に、操作構文は次のようになります。

match with

[|]1|2|..|10 -> 1

|11 when 1 -> 2

...







テンプレートとの比較は上から下に行くため、幅の狭いテンプレートは上に配置する必要があることを忘れないでください。 最も一般的なテンプレートは次のようになります:_(アンダースコア)。これは、識別子の値に関心がないことを意味します。 さらに、テンプレートとの比較が完了し(未調査の機能はありません)、すべての計算で同じタイプの結果が生成されます。

テンプレートを使用した最も単純なタイプの操作では、識別子と特定の値(数値、文字列)を比較します。whenキーワードを使用すると、テンプレートに条件を追加して、計算を実行できます。

別の識別子が値に置き換えられる場合、検証された識別子の値がそれに割り当てられます。

テンプレートと比較するために最も一般的に使用されるオプションは、タプルとリストの上にあります。 xを(string * int)形式のタプルとすると、同様のテンプレートを作成できます。

match x with

| "" , _ -> ", !"

| _, i when i > 200 -> ", !"

| name, age -> sprintf " %s, %d" name age

| _ -> " "







テンプレートに識別子がある場合、対応する値によって識別子が自動的に決定され、処理で名前フィールドと年齢フィールドを別々に使用できることに注意してください。

リストはまったく同じ方法で処理されます(これは実際にはリストではなく、以下で説明する差別化された結合です)。 通常、リスト(「リスト」)のテンプレートは、空の場合は[]として、またはヘッド::タイプがヘッドのテールで、テールがタイプ 'aリストのいずれかです。ただし、他のオプションも可能です。

match lst with

|[x;y;z] -> //lst , xy z.

|1::2::3::tail -> // lst [1,2,3] tail







テンプレートと比較するときに識別子に値を渡す機能は非常に便利なので、F#では、次のようにテンプレート構文を使用せずに、そのような割り当てを直接行うことができます。

let (name, age) = x







またはそれでも:

let (name, _) = x







タプルの最初の要素のみに関心がある場合。



投稿



F#のレコードはタプルに似ていますが、それらの各フィールドには名前があります。 エントリの定義は中括弧で囲まれ、セミコロンで区切られます。

type org = { boss : string; tops :string list }

let Microsoft = { boss = "Bill Gates" ; tops = [ "Steve Balmer" , "Paul Allen" ]}







レコードフィールドには、通常どおり、ポイントを介してアクセスします。 以下に示すように、エントリはクラスを模倣できます。



タグ付きプール



F#のこのタイプを使用すると、異なる構造と意味を持つデータを保存できます。 たとえば、ここにタイプがあります:

type Distance =

|Meter of float

|Feet of float

|Mile of float



let d1 = Meter 10

let d2 = Feet 65.5







3種類のデータはすべて同じタイプ(オプション)ですが、明らかに意味が異なります。 ラベル付き結合は、常にテンプレートとの比較を通じて処理されます。

match x with

|Meter x -> x

|Feet x -> x*3.28

|Mile x -> x*0.00062







すでに述べたように、リストなどの一般的なデータ型は、マークアップされた結合です。 その非公式の定義は次のようになります。

type a' list =

|[]

|:: of a' * List







ところで、上記の例からわかるように、F#のマークアップされたセットは、一般的な方法でパラメーター化できます。



F#命令的





ユニットタイプ



ユニットタイプは、C#のvoidタイプに関連しています。 関数が引数を取らない場合、その入力タイプはユニットです;値を返さない場合、その出力タイプはユニットです。 関数型プログラミングでは、値を受け入れない、または値を返さない関数は値を表しませんが、命令型パラダイムでは、副作用(たとえば、入出力)のために値を持ちます。タイプunitの唯一の値の形式は()です。 このような関数は何も受け入れず、何も行いません(ユニット->ユニット)。

let doNothingWithNothing () = ()







名前の後の括弧は、これが値ではなく、空の入力を持つ関数であることを意味します。 前述したように、関数は値ですが、関数値と非関数値には大きな違いがあります。2番目は1回だけ計算され、1番目は呼び出しごとに計算されます。

値を返す関数はすべて、ignore関数を使用してユニットタイプを返す関数に変換できます。 それを適用して、この関数では戻り値ではなく副作用にのみ関心があることをコンパイラに伝えます。



可変キーワード



知っているように、一般的な場合、F#の識別子は何らかの値で定義できますが、この値は変更できません。 ただし、古き良き命令型変数は依然として有用であるため、F#は変数を作成および使用するためのメカニズムを提供します。 そのためには、変数名の前にmutableキーワードを記述する必要があり、値は<-演算子を使用して変更できます。

let mutable i = 0

i <- i + 1







ただし、このような変数の使用は制限されています。たとえば、ラムダでのクロージャと同様に、内部関数では使用できません。 このコードはエラーをスローします:

let mainFunc () =

let mutable i = 0

let subFunc () =

i <- 1









タイプref



F#では、 ref型を使用して変数を定義する別の方法があります。 これを行うには、識別子の値を表す計算の前にキーワードrefを置きます。

別の値を変数に割り当てるには、ノスタルジックな演算子:=を痛々しいほど使用します。変数の値へのアクセスは追加することで行われます! 変数名の前。

let i = ref 0

i := !i + 1







おそらく、この表記は前の表記ほどきれいではありません。感嘆符を使用して値を取得するだけの価値があります(Fの否定には#キーワードはありません)

ただし、 mutableとは異なり、 ref型にはスコープに関する制限がないため、ネストされた関数とクロージャーの両方で使用できます。 そのようなコードは動作します:

let i = ref 2

let lst = [1..10]

List.map (fun x -> x * !i) lst









配列



F#には、可変型の配列があります。 リスト内の値とは異なり、配列内の値は再割り当てできます。 配列はそのような角括弧[| |]、その中の要素はセミコロンでリストされます。 配列の要素へのアクセスは[Ind]を介して行われ、代入はmutableの操作に慣れている<-演算子によって実行されます。 配列を処理するためのすべての関数(リストを処理するためのメソッドにほぼ類似)は、Arrayクラスにあります。

let arr = [|1; 2; 3|]

arr.[0] <- 10 // [|10,2,3|]







配列は、..、yieldなどを使用して、リストとまったく同じ方法で初期化できます。

let squares = [| for x in 1..9 -> x,x*x |] // [| (1,1);(2,4);...;(9,81) |]







また、F#を使用すると、「ステップ」(長さの異なるサブ配列を使用)と「モノリシック」の両方の多次元配列を作成できます。



制御ロジック



F#では、通常の命令型制御ロジック-条件演算子if ... then ... else 、およびforループとwhileループを使用できます。

ifステートメントは関数と見なすこともできることに注意してください。つまり、どのような条件下でも同じ型の値を返す必要があります。 これは、 elseの使用必須であることも示唆しています 。 実際、例外が1つあります。計算が行われ、条件が満たされたときに、タイプ単位が返されます。

if System.DateTime.Now.DayOfWeek = System.DayOfWeek.Sunday then

printfn " !"

printfn " !"







シフトは、ループに属する関数とそうでない関数を判別するためにも使用されます。 たとえば、上の例では、曜日に関係なく2番目の文が表示されます。

F#のforループはユニット型であるため、ループ本体での計算によりこのタイプが生成されます。そうでない場合、コンパイラーは警告を出します。

let arr = [|1..10|]

for i = 0 to Array.length arr - 1 do

printfn arr.[i]







逆方向に進みたい場合は、古き良き時代のようにtoを downtoに置き換えます。

また、おなじみのforeachに似たforループの別の形式を使用することもできます。

for item in arr

print_any item







whileループも非常に一般的で命令型プログラマーに馴染みがあり、その本体はキーワードdodoneの間にありますが、2番目のループはオプションでシフトシステムを使用して省略できます。



.NETライブラリから静的メソッドとオブジェクトを呼び出す



F#で.NETツールキット全体を使用できますが、F#で記述されていないすべてのメソッドにはカレー機能のプロパティがないため、入力要素のセットに対応するタプルの形式で引数を指定する必要があります。 この場合、コールレコードはsyncharpと1つのiotaを区別しません。

#light

open System.IO

if File.Exists( "file.txt" ) then

printf " !"







ただし、.NETメソッドに本当に好奇心を持たせたい場合は、次のようにインポートする必要があります。

let exists file = File.Exists(file)







オブジェクトの使用も同様に簡単です。 新しいキーワード(だれが考えたでしょうか?)を使用して作成され、コンストラクターパラメーターの適切なタプルを使用します。 letを使用してオブジェクトに識別子を割り当てることができます。 メソッド呼び出しは静的に似ており、フィールドは<-を使用して変更されます。

#light

let file = new FileInfo( "file.txt" )

if not file.Exists then

using (file.CreateText()) (fun stream ->

stream.WriteLine( "Hello world" ))

file.Attributes <- FileAttributes.ReadOnly







F#では、オブジェクトを作成するときに、次のようにフィールドをすぐに初期化できます。

let file = new FileInfo( "file.txt" , Attributes = FileAttributes.ReadOnly)









Fでイベントを使用する#



F#の各イベントには、イベントにハンドラー関数を追加するAddメソッドがあります。 ハンドラー関数は、タイプ 'a- > unitでなければなりません。 タイマーイベントにサインアップする方法は次のとおりです。

#light

open System.Timers

let timer = new Timer(Interval=1000, Enabled=true)

timer.Elapsed.Add(fun _ -> printfn "Timer tick!" )







イベントからのサブスクライブ解除は、Removeメソッドを使用して行われます。



演算子|>



転送演算子|>は次のように定義されます。

let (|>) fg = gf







最初の引数をパラメーターとして2番目の引数に渡します。 もちろん、2番目の引数は、唯一のパラメーターとしてf型の値を取る関数でなければなりません。 ちなみに、転送演算子を使用する可能性があるため、リストの上にあるすべての関数(iter、map、fold)はリスト自体を最後に取得します。 次に、gとして、劣決定関数を使用できます。

[1..10] |> List.iter (fun i -> print_int i)







たとえば、iter関数の形式は( 'a list- > unit) -> ' a list- > unitです。最初のパラメーターをラムダで設定すると、 'a list- > unit型の関数が取得されます。これは引数として定義されたリストのみを引数として受け取ります。

プログラムは、多くの場合、転送演算子の長いチェーンを使用します。各演算子は、前の演算子で取得した値、つまり一種のパイプラインを処理します。



F#オブジェクト指向



今日のプログラミングの主力であるのはオブジェクト指向のパラダイムであると主張する人はほとんどいないと思います。もちろん、F#はそれに含まれる概念を無視できませんでした。 彼が私たちに提供してくれるものを見てみましょう。



タイピング。



F#では、静的な値の型を明示的に変更することができます。 F#は2つの異なる演算子を使用して上下にキャストします。 起動、つまり、静的型にその祖先のいずれかの型の値を割り当てることは、演算子:>によって実行されます。 下の例のstrObj値は、オブジェクト型になります。



let strObj = ( "-, -" :> obj)







代入、つまり、子孫のいずれかの型による値の型の指定は、operator:?>によって実行されます。

値のタイプ(アナログはC#のもの)を確認するには、演算子:?を使用します。これは、論理構造だけでなく、テンプレートと比較するときにも使用できます。

match x with

|:? string -> printf " !"

|:? int -> printf " !"

|:? obj -> printf " !"







通常、F#は、関数を計算するときに型継承の階層を考慮しません。つまり、型継承を引数として使用することはできません。 たとえば、このようなプログラムはコンパイルされません。

let showForm (form:Form) =

form.Show()

let ofd = new OpenFileDialog();

showForm ofd







原則として、明示的に型をキャストできます:showForm(ofd:> Form)、ただし、F#は別の方法を提供します-関数シグネチャの型の前にポンド記号#を追加する。

let showForm (form: #Form) =

form.Show()







したがって、特定の関数は、Formから継承したクラスのオブジェクトを引数として受け取ります。



オブジェクトとして記録および結合



レコードと結合にメソッドを追加できます。 これを行うには、レコードを定義した後、withキーワードを追加し、すべてのメソッドを定義した後にendを記述し、各メソッドの識別子の前にmemberキーワードを使用する必要があります。

type Point ={

mutable x: int;

mutable y: int; }

with

member p.Swap() =

let temp = px

px <- py

py <- temp

end







メソッド名の前に指定されたパラメーターpは、フィールドにアクセスするためにその中で使用されることに注意してください。



クラスとインターフェース



F#のクラスは、キーワード用いて決定されているタイプのクラス名、等号キーワードに続いて、クラスクラス定義はendキーワードで終了します。コンストラクターを指定するには、クラス定義にnewという名前のメンバーを含める必要があります

type construct = class

new () = {}

end

let inst = new construct()







クラス定義には少なくとも1つのコンストラクターが含まれている必要があります。そうでない場合、コードはコンパイルされません。F#は、C#のようなデフォルトのコンストラクターを提供しません。

フィールドを定義するには、その名前の前にキーワードvalを追加する必要があります

type File = class

val path: string

val info : FileInfo

new () = new File( "default.txt" )

new (path) =

{ path = path;

info = new FileInfo(path) }

end

let file1 = new File( "sample.txt" )







ご覧のとおり、コンストラクタは通常の方法でオーバーロードできます。コンストラクターは一部のフィールドを初期化せずに残すことはできません。そうしないと、コードはコンパイルされません。コンストラクターでは、フィールドの初期化または他のコンストラクターの呼び出ししかできないことに注意してください。コンストラクタで追加の操作を指定するにその後にを追加し、その後にすべての追加の計算を記述する必要があります。

new (path) as x =

{ path = path;

info = new FileInfo(path) }

then

if not x.info.Exists then printf " !"







デフォルトでは、クラスのフィールドは不変です;フィールドを可変にするには、その名前の前に可変を追加する必要があります

F#のインターフェイスは、次のように定義および実装されます。

let ISampleInterface = interface

abstract Change : newVal : int -> unit

end



type SampleClass = class

val mutable i : int

new () = { i = 0}

interface ISampleInterface with

member x.Change y = xi <- y

end

end







F#は、クラスを定義する別のエレガントな方法-暗黙的な割り当てを提供します。クラス名の直後に入力パラメーターがリストされます。そうでなければ、コンストラクター引数に含まれます。クラスの構築は、メソッドの定義に先行するletシーケンスの助けを借りて、その本体で直接行われます。この方法で定義されたすべての識別子は、クラスに対してプライベートになります。クラスのフィールドとメソッドは、memberキーワードを使用して設定されます。すぐに例を参照することをお勧めします。

type Counter (start, inc, length) = class

let finish = start + length

let mutable current = start

member c.Current = current

member c.Inc () =

if current > finish then failwith "-!"

current <- current + inc

end



let count = new Counter(0, 5, 100)

count.Inc()







F#はC#のように、単一の継承のみをサポートし、複数のインターフェースを実装します。継承は、キーワード使用して指定された継承直後のクラスを



type Base = class

val state : int

new () = {state = 0}

end



type Sub = class

inherit Base

val otherState : int

new () = {otherState = 0}

end







明示的に基本的な空のコンストラクターを呼び出す必要はありません。子孫コンストラクターが呼び出されると、空の祖先コンストラクターが自動的に呼び出されます。祖先にそのようなコンストラクターがない場合は、inheritキーワードを使用して、子孫コンストラクターで基本コンストラクターを明示的に呼び出す必要があります

F#のプロパティは次のように定義されます。

type PropertySample = class

let mutable field = 0

member x.Property

with get () = field

and set v = field <- rand

end







静的フィールドを決定するための部材キーワード追加静的クラスの除去パラメータobznachayuschyインスタンス。

type StaticSample = class

static member TrimString (st:string) = st.Trim()

end









おわりに





今日、私たちは言語の基本的な機能のほとんどを簡単に調べ、見たものに基づいていくつかの結論を導き出すことができます。

実際、C#で使用可能なクラスと変数を使用した操作はF#でも実行できるため、この言語は、お兄さんほどオブジェクト指向ではありません。その中の構文はおそらくもっと複雑ですが、重要ではなく、それは習慣の問題のようです。一方、機能面では、F#、予想通り(実際、ここで説明されているので、さらに予想されるはずです)



All Articles