LED通信事業プロジェクト エンジニアブログ

F#でラズパイを動かす(4)

F#で「Lチカ」 Part.1

記事更新日 2022年6月7日


はじめに

小さくて安い教育用コンピューターであるRaspberry Pi通称ラズパイですが、このラズパイがここまで広まることとなった最大の理由はプログラムによって電圧を制御することができる物理ピンを装備していることです。写真で赤で囲まれた部分がそのピンになります。

fig.1
図1:Raspberry Piのピン

このピンを使うと、LEDをつけるとか、ICと繋げてモーターを制御するとか、センサーと繋げてデータを取得するなど「リアル」な動作が可能です。今時のWindowsパソコンやMacコンピューターを思い浮かべてみてください。外部装置との接続口はUSBか、それに加えて精々LANポートしかありません。これらポートはデジタルな「データ」のやり取りは可能ですが、ものを動かすとか、ICと繋げるとか、「リアル」な動作は行われませんよね※1。LEDやモーターなど「リアル」なものを(ICなどを通じて)電気的に操作すること、またそういったものを作ることを一般に「電子工作」と呼びますが、電子ピンを持つラズパイはこの電気工作に最適で、高いシェアを誇ります。

ちなみに電子工作を学習する際に一番最初にやること(つまり一番手っ取り早く簡単にできること)はLEDを点滅させることです。LEDをチカチカさせるため、通称Lチカと呼ばれます。プログラミングでいうHello Worldと同じ扱いで、ラズパイの電子工作のどんな学習書を読んでもまず初めにすることといえばLチカです。当然、ラズパイ電子工作でググれば沢山の「Lチカ」を見つけることができると思います。単純にいえばラズパイのピンにLEDを繋ぐだけでLEDを光らせることが(当然消すことも)できるため、誰でも簡単に始められます。

しかし、ひとつとても残念なことがあります。それは、私は寡聞にして「F#でLチカ」の記事は見たことがない、ということです。ラズパイでF#は誰もやっていないのか・・・

仕方ない、誰も書かないなら自分で書くしかない。そんな訳で「F#でLチカ」を今回から3回に分けて書かせて頂きます。1回目は主にF#の基本的なルールの説明、二回目は具体的なLチカプログラミングの説明、3回目はラズパイとLEDの接続を含めたLチカの説明となります。

この連載は、ほんの少しでも「プログラミングというものをやったことがある」という人をターゲットにしています。(VBAやJavaScriptを少し書いたことがある、程度を想定)ですので、プログラミングの最低限のルール的なものは説明いたしませんので、あらかじめご了承ください。

F#の特徴的なところ

このブログでF#の文法的な細かい部分まで網羅するのは無理なので、ここではLチカを書く上で理解が必要なF#の特徴的な部分をピックアップして説明します。細かい文法などは、MSの公式ページこのページなどを参考にするのが良いと思います。紙の書籍も少ないものの存在していて、私も昔買いましたが、残念ながら古いため今や入手困難ですし、しかもF#はここ数年で(特にAI向けに)大幅に機能が追加されていたりしますので、リアルタイムで更新されるWeb上の情報の方が参考になると思います。

それでは、今回使うことになる特徴を5つほど説明していきます。

1. 型宣言がない

一般的なプログラミング言語で最初にやらなきゃいけないことといえば変数の宣言でしょう。 まず、VBAやVB.NETでの変数宣言を見てみましょう。例えば整数の変数を宣言するのに次のように書きます。

'VBでの型宣言 Dim a as integer a = 1

これにより整数が入れられるの”a”という変数の設定ができます。型宣言と呼ばれる作法で、殆どの言語で使われています。そして、型宣言をした後に、aに自由に値を入れられます。この型宣言は、コンピューター上では「メモリ領域の確保」の意味をもっており、aはメモリ領域(整数型なら4byte)を確保し、自由に読み書きできるようにしたことを意味します。

それをF#でやるとこうなります。

//F#での型宣言? let a = 1

これでaは1という整数と宣言されましたが・・・

F#においてのこの宣言は変数宣言ではないのです。F#における宣言語であるletは束縛(バインド)と呼ばれていて「何かと何かを同じものだと見なす」ということを意味します。この例ではaと1という整数が同じものであるという宣言であり、aは1以外の値はとりません。つまりaは変数とは呼べませんし、固定値なので「メモリ領域の確保」という意図もありません。当然代入もできません。F#では「基本的」に型宣言をしない、そして変数を使わないのです。これは関数型言語の特徴です。コンピューターの性能が上がり、メモリの細かいやり取りを人間が気にしなくてもよくなったからこそ実現した言語とも言えます。

と偉そうに言いましたが、実はF#にも変数は存在します。他の関数型言語では変数を全く持たない言語もありますが、F#はオブジェクト指向である.NET言語の性質も併せ持つため、他の言語との互換性を維持するため変数を無くすことができませんでした。例えば、いろいろなオブジェクトや、値を入れ替えながら使いたい配列など、F#では変数が必要になるケースが少なくありません。そのようなときのためにF#では型宣言の代わりとなるような宣言方法が準備されています。

// F#の変数宣言 let mutable a = 0 //aを変数として宣言 a <- 1 //aに1を代入

letmutable(日本語で言えば「可変」)という単語が追加されました。mutableを追加するとaは変数と見なされます。これで他の言語と同じように変数の宣言ができます。ただし、F#ではIntegerとかStringとか、型の名前を使って変数を宣言することができません。そのため、最初に代入した値(数字や文字やリスト)の型で宣言されます。上の例では0という値で宣言したため、aは整数型になっています。そして、文字列で宣言すれば文字列型に、true/falseで宣言すればブール型になります。他言語と同じように、一度宣言した変数の型を変更することはできません。変数に対する代入は<-で行います。F#において=は「式が等しい」ことを表す演算子であって、C#やVB.NETや多くの他言語のような代入を表す演算子ではありません。

というわけで・・・ 関数型言語であるF#では通常変数を宣言しませんが、NET準拠でもあるためmutableを使うことでC#やVB.NETと同じように変数の宣言ができ、同じような書き方”も”できます。ですが、せっかく関数型言語F#でコード書くのですから、今後の記事では「極力変数を使わないのがF#における美しい書き方であるとして、仕方ない場合のみ変数を使う」という方針で進めさせて頂きます。(といっても、変数は結構出てきてしまいます。)

この章の最後に、参考までに様々な型の宣言例を挙げておきます。F#は型に非常に厳密な言語なので、特に数値型の使い分けは覚えておいてください。

// 様々な型の宣言 let mutable a = 0 // int(整数型) let mutable b = 0L // long/int64 (長整数型) let mutable c = 0.0f // single/float32 (単精度浮動小数点型) let mutable d = 0.0 // double/float (倍精度浮動小数点型) let mutable e = 0uy // byte (バイト型) let mutable f = true // bool (ブール型) let mutable g = "abcde" // string(文字列型)

2. 関数の書き方と実行ルール

F#は関数型言語ですから、まずは関数の書き方から簡単に説明します。基本的なF#の関数の書き方は次の例1のようになります。

// 例1 // a + bという関数 let tashizan a b = a + b let answer = tashizan 3 4 // answer = 7

aとbという引数を取り、a + bの答えを戻り値とするtashizanという関数の例です。見ての通り、関数の書き方は

  • let 関数名 引数1 引数2 ...= 計算の中身

となります。関数名と引数の間や、引数と引数の間は「スペース」を入れます。引数は何個つけても構いません。C#やVB.NETは引数は()の中に書きますが、F#では”通常”書きません。通常と言ったのは()の中に「書いてもいい」からですが、ちょっと条件がややこしいのでこれは後程説明します。

関数から別の関数を作る、といったことも簡単にできます。

// 例2 let tashizan a b = a + b // 最初の関数 let tashizan2 = tashizan 5 //最初の関数から引数を減らす関数を定義(カリー化と呼ぶ) let answer2 = tashizan2 6 //answer2 = 11

F#には参照される関数はその行よりも前の行に書かれていなければならないというルールがあります。C#やVB.NETでは、関数がどこに書かれていようが、その行から「見える位置」に書かれていればその関数を呼び出すことができました。F#では絶対に前、つまり上に書かれていなければ関数を呼び出せません。これは関数だけでなく、変数や後で出てくる「クラス」でも同じルールが適用されます。このルールはF#の型推論※2の方法からして当然のルールなのですが、他言語に慣れた方であればあるほど違和感があり引っかかるルールだと思いますのでご注意下さい。

さて、F#では全てが関数ということになっていて、上記例では全てletが付いていて全て同じように見えます。しかし、参照されるためにある「関数」と、実行されるためにある「実行文」は異なります。その違いは引数の有無です。引数が付いている文は関数と認識され、呼び出し時に使われます。引数が付いていない文は今すぐ実行する文として認識され、計算されます。例えば、下の例3を見ると、上2行は関数、最後の行だけが実行文のため、実行されるのは最後の行となります。

// 例3 let tashizan a b = a + b // 関数 let tashizan2 b = tashizan 5 b//関数 let answer2 = tashizan2 6 //引数がないので実行

当然のことながら、関数は複数行に跨がって書くことも可能です。また、関数の中に関数を入れることも可能です。例えば例4の様に書くことができます。

// 例4 // 複数行関数の例 let tashizan a b = a + b //足し算の関数 let kakesan a b c = let hikizan x y = y - x //関数 let a1 = tashizan a b //バインド(a1 = 3) let a2 = hikizan b c //バインド(a2 = 3) a1 * a2 //ここがkakesan関数の戻り値 (a + b) * (c - b) let answer3 = kakesan 1 2 5 //answer3 = 9

関数を複数行で書く場合は1段以上インデントします。F#はインデントで関数が途中なのかどうなのかを判断します。C#でいうところの{}の機能をインデントにより表現しています※3。インデントは常に意識する必要がありますが、インデントが間違っているとVisual Studio(VS)に即座にエラーだと怒られますので、書いていくうちに否が応でも身につくと思います。

本題に戻りまして、複数行のkakesanという関数について見ていきます。kakesanという関数の中身は5~8行目です。5行目はhikizanという別の関数が定義されています。hikizan関数はkakesan関数の中で宣言されているため、kakesan関数の中でのみしか使用することができません。次に、上の3行(5~7行目)はletで宣言されていますが、8行目のa1 * a2はletなど頭に付かず宣言文ではありません。F#において宣言されていない行は、戻り値を上位(上のインデント)に返すことを意味します。kakesan関数でいえばkakesan = a1 * a2 です。そして、a1とa2が何かは前で宣言されていますので、それを参照して計算が行われるという訳です。

宣言の時に出てきたlet a = 1というのも1行で書いているために違うように見えますが実は全く同じ構造でして、この文を複数行で書けば次のようになります。

// 例5 let a = 1 //aに対する戻り値

1はletで宣言されていないため「上位のaに対する戻り値になる」となることが分かると思います。

F#では、if文も関数扱いで戻り値を必要とします。次の例は、引数がプラスかマイナスかを判定するためのif文です。このif文は何か(代入とか)を実行するのではなく、isPlusに結果を返すという関数になっています。どのような結果でも必ず戻り値を返す必要があるため、else以下を省略することはできません。

// 例6 let isPlus x = if x >= 0 then true //正の整数ならtrueの戻り値 else false //負の整数ならfalseの戻り値

3. unit

では戻り値の要らない式はどうすればいいでしょう。例えば、前回出てきたprintfn "Hello world!"は、一般的にメソッドと呼ばれます。例えば画面に何かを表示させたり、タイマーをスタートさせたり、メモリをクリアしたり、そういったものがメソッドです。メソッドは何かを実行する命令です。そのため通常戻り値を持ちません。また、ちょっと前に出ているa <- 0という代入式も、代入しているだけですので戻り値は要りません。関数型言語のF#で、メソッドや代入式はどのような扱いになるのでしょうか?

実は、どちらの式も戻り値が発生しています。F#では、どんな式でも必ず戻り値が発生します。そして、F#では、通常戻り値が要らないメソッドや代入式において自動的に発生する(させる)戻り値は決まっており、それはunitと呼ばれます。

例えば、次の例7ですとa, bいずれもunitが代入されます。

// 例7 let a = printfn "Hello world!" //a = unit let b = a <- 0 //b = unit

unitはunitという型のunitという値と定義されていて、何も意味を持ちません。unitは計算も変形もなにもできません。関数型言語であるF#とオブジェクト指向言語向けにつくられた.NETとの整合性をとるために、つじつま合わせでつくられた型であり値です。しかし、このunitという値を理解することがF#のポイントの一つとも言えます。

このunitは、戻り値に使われるだけでなく、引数でも使われます。例えば、何も引数を持たない関数を作りたい場合、式に引数を入れずに関数を作ってしまうと「実行式」と判断されてしまい、計算が先に行われてしまいます。そうならないため、つまり関数として認識させるため、引数としてunitを指定します。そうすれば、引数が実質的にない関数を作ることができます。例えば、代入式を例にとって説明してみます。

// 例8 let mutable a = 1 let function1 = a <- 0 //実行式なので自動的に実行される(a = 0) let function2() = a <- 2 //関数(戻り値unit)なので他で呼び出すまで実行されない do function2() //ここでfunction2が実行される(a = 2)

unitを明示的に表したい場合は()と書きます。F#では()はunitを意味します。上の例では、2行目は実行式となるため実行されますが、3行目は関数なので実行されません。そして、4行目でfunction2を呼び出すことにより、function2のa <- 2が実行されます。4行目では戻り値unitの関数を呼び出すため、戻り値を無視することを表すdoをつけています。(VB.NETでもメソッド実行にdoをつけますが、それと同じです)

前章でif文は関数だと言いましたが、例えばメソッド実行や代入だけを行いたいようなif文を書きたいときは、unitが戻り値になるように書かなければなりません。そして、unitが戻り値のif文ではすべての条件でunitが返るように書かなければなりません。

// 例9 let mutable a = false let b = 5 //結果的に”dainyu=unit”となるif文 let dainyu = if b >= 0 then a <- true //代入文の戻り値unitを返す else () //何も処理がなくともunitを返す

4. エントリーポイントと実行の順番

プログラミング学習で「意外と困る」事のひとつに、「プログラムはどこから実行されるのか?がわからない」という事があります。言語によってルールが異なって結構理解が面倒なものです。F#は最初の実行位置を動的、静的を選ぶことができます。動的というのは、コードを自動判別して最初の行を選択することで、F#のルールを把握していないと、最初の行の判別が困難です。一方、静的とは、プログラム最初の行を明示的に宣言するという方法です。これなら簡単にプログラミング実行開始ポイントを作成できます。例10をご覧下さい。

// 例10 [<EntryPoint>] let test args = // ここに処理を書く 0 //戻り値(int)

一番最初に実行したい関数の上に[<EntryPoint>]を付けるだけで、そこが開始する行(エントリーポイントと呼びます)になります。たったそれだけです。例10はコマンドプロンプトで動くアプリのスタートの例です。ラズパイのアプリは、基本的にコマンドプロンプトで動くアプリになるため、この様に書けばコマンドプロンプトアプリのエントリーポイントが作れます。

コマンドプロンプトアプリには一つルールがあり、コマンドプロンプトで一番初めに動かす関数は、引数が1つ必要で、戻り値は必ず整数(通常0)に設定する必要があります。関数名も、引数名も自由に決められますが、引数は必ず一つ※4、戻り値は正数でなければエラーとなります。

整数値がコマンドプロンプトに返るとプログラムは終了となるため、この戻り値が来るまでに全ての処理が終わっている必要があります。処理中のものがあっても※5戻り値のところに来てしまうとそこでプログラム全体が終了となります。0以外の整数値を返してもいいですが、0以外は通常エラー終了を意味する数値となるため、通常は0を戻すのが無難です。

さて、思い出して欲しいのが「F#では参照される関数は上(前)に書いていなければならない」というルールです。このエントリーポイントで関数を参照したいなら、その上に書かなければなないということなのですが、これを逆に言うと、エントリーポイントをつけた関数の戻り値より下(後)に書いた関数や処理は全て無視されるということでもあります。つまり、エントリーポイントは、一番下(最後)に書くべきものである※6ということを意味します。

5. タプル

今時の言語にはタプルという考え方があります。二つ以上の要素(変数やオブジェクト)を組み合わせ使うことです。一番簡単な例は(x, y)のような座標の組み合わせ。このような組み合わせを一つの要素として扱うことができるのがタプルです。2つ以上なら何個組み合わせても構いませんが、順序には意味があります。これだけだと普通の配列(Array)と同じですが、タプルは異なる型の組み合わせで使うこともできるというのがミソです。例11のように使います。

// 例11 // タプルの使用例 // デシベルに変換する関数(対数は0以下の数値をとれない) // 戻り値は(bool, float)のタプルとなっている let dB a = if a > 0.0 then (true, 10.0 * log10(a)) else (false, 0.0) let (isOK, answer) = dB 20 //isOK=true, answer = 13.01029996 let (isOK, answer) = dB -1 //isOK2=false, answer = 0.0

実数からデシベル(dB)へ変換する計算は10*log10(a)ですが、対数の中身は正数(0より大きい数)であることが求められるため、aが正数で無い場合はエラーとなります。ただ、エラーであるかどうかを単一の数値の戻り値で表すのは困難なため、1番目に計算できたかどうかを表すbool型、2番目に計算結果であるfloat型の値をもつ「タプル」で戻り値を構成しています。これによって、この関数を受け取る側は一つ目の戻り値を見るだけで、正しく計算できたかどうかが分かるようになっています。このように、複数の要素を一つにまとめて戻り値として使えるというのがタプルの便利なところです。

このタプル、同じ.NETのC#やVB.NETでも使えるのですが、使えるようになったのは2017年からで、結構最近の話です。ですから、C#を使っていても、タプルは使ったことないという人は多いかも知れません。正直、C#、VB.NETであれば、タプルを使わなくてもあまり困りませんし、代用できる方法はいくつもあります。例えば、2つ戻り値を使いたいなら引数を参照型にするという手もあります。タプルのない時代にはよく使われていた方法で、.NETでよく使われる様々な型のチェックをするための関数TryParseでもこの手法が使われています。

' 例12 'VB.NETでのTryParseの例 Dim a as integer = 0 Dim sample as string = "10" Dim isInt = Integer.TryParse(sample, a) 'isInt=True, a=10

この例12は、第一引数が整数型に変換できるかどうかを調べて、変換できるならば戻り値にTrueを返しつつ、第二引数に変換した数値を返すというInteger.TryParse関数の使用例です。ちなみに.NETではInteger型だけでなく様々な型でも同じ関数が存在します。F#でも当然この関数は使えるのですが・・・ F#でTryParse関数を使うためには、参照型引数に入れるため別途mutableで宣言して変数を作る必要があります。変数は極力使いたくないということもありますが、そもそも引数に関数を入れることも少なくないF#では、この参照型を強いるような関数を、積極的に使う理由はありません。

また、引数にタプルを使うと関数として意味が変わることがあります。先ほど、F#の引数は括弧を付けない、ということを書きました。しかし、括弧を付けて書くこともできます。下の例13をご覧下さい。

// 例13 // この二つは全く同じ let function1_1 a = a + 1 let function1_2 (a) = a + 1 // 2_1と2_2は同じだが、2_3は違う意味を持つ let function2_1 a b = a + b let function2_2 (a) (b) = a + b let funciton2_3 (a, b) = a + b // 2_1と2_2はこういう使い方ができる、2_3はできない let newfunction2 = function2_1 10 //関数の再定義(カリー化) let resutl = newfunction2 5 //result = 15 // ただし2_3はこういう使い方ができる let c = (10, 5) let result = function2_3 c //result = 15

function1_1と1_2は引数の括弧の有無が異なりますが、意味も使い方も全く同じです。一つの引数に対して括弧をつけても、付けなくても同じ意味になります。また、function2_1と2_2も全く同じです。2_3はどうでしょう。一般的な言語においては、例えばC#でもVB.NETでも関数は通常2_3の様な書き方をすると思います。しかし、F#において2_3の書き方をすると、引数はタプルであると見なされます。

タプルであることは、引数が必ずセットで準備されることを意味します。これは、メリット、デメリットあります。例えば、引数でタプルを使ってしまうと、F#の売りの一つである関数の連結(パイプライン)や関数の合成※7などができなくなってしまいます。その代わり、タプルのまま代入できるというメリットもあります。例えば、必ず組み合わせて一緒に使う引数(例えば、座標とかパラメータの組み合わせとか)であれば、タプルになっていた方が扱いやすいはずです。両者は使い分ければ良いと思いますが、普通の関数であればタプルを使わない方が、使い回しなど考えれば良いと思います。

まとめ

これを理解しないとF#が理解できないという特徴的なところを5つ説明させて頂きました。次回はいよいよLチカのためのプログラミングコードそのものの説明を指定きたと思います。

最後に、プログラミングのプロではないので、多少表現や語句などに厳密で無い場合もあるかも知れません。そのあたりはご容赦頂きつつ、ぜひF#に挑戦して頂ければと思っております。


※1; 一応、USB接続でラズパイのような物理ピンを外付けことのできるデバイスも存在するため、操作できないわけではない。

※2; コードから関数(引数、戻り値)の型を推測すること。明示的に型を宣言しないF#において、その型は代入時、呼び出し時に(ツールによって自動的に)決定される。

※3; C#とVB.NETは、VS上で書くと、オートインデントが働きVSが自動的にインデントを追加・削除してくれます。ですが、両言語ともインデントは「見た目」わかりやすいためにかかるものであり、プログラミングの中身には影響ありません。F#はインデントの位置でコードの意味が変わるため、オートインデントは働かない。

※4; この引数は、コマンドプロンプトでアプリ実行時の「オプション」が入力されるため、引数の型はstring[](文字列の配列)と決まっています。それ以外だとエラーになるが、敢えて引数の型指定をしなければ自動的にstring[]型になりますので気にする必要はない。

※5; .NETは通常にコードを書くと同期式なので処理途中のものは発生しないが、非同期処理やタイマー処理等を行っている場合には注意が必要。

※6; エントリーポイントが最後でないとエラーが出るとかそういうことはない。単純に、エントリーポイントの戻り値より下は無駄になるという意味。

※7; これを簡単に説明するのは難しいため、本記事では中身まで触れない。知りたい方はMicrosoft公式ページを参照のこと。