新しくプログラム言語を勉強・習得するときの手順をまとめてみたよ(F#篇)

初心者向けのなんとかがこの世に溢れかえっていますが実際にアプリを作るまでの手順がなかったりするのでまとめてみました。
全く知らない言語を習得しながらアプリケーションを作成するまでを書いています。
言語はF#で解凍ソフトっぽいものを作ってみます。
今回の記事は超長いよ!!気合入れて読んでね♪







環境


Windows8.1
VisualStudio2015 Community



この記事書いている人の知識レベル


前提が全く違うと参考にならないと思うので私の知識レベルを書いておきます。
  • 業務ではC#,Java,PHPをよく使う
  • 関数型言語の経験は全くなし
  • C#のlinqは大好き
  • モナドとか副作用のないプログラムとかは聞いたことがあるかもしれない
  • 圏論......なんですかそれは?



F#ってこんな言語らしい


ググってみた
  • OCamlをベースに作られた関数型言語とのこと
  • MS製で.netFrameworkで開発可能
  • 誰かが 「いいプログラムを書くために強制ギプスをされたよう」 と表現していた
  • スクリプトとしても使用可能らしい



何をやるのか


zipファイルを解答した時にデスクトップにファイルが広がったり、1階層フォルダが深くなったことありますよね?
それをイイカンジ(SE的表現)にするアプリケーションを作ってみます
具体的には以下のことがやりたい!
  • zipできればrarに対応したい
  • 解凍時に直接ファイルが圧縮される場合zipファイル名で解凍、フォルダが圧縮されている場合そのまま解凍
  • 右クリックから解凍出来るようにしたい
  • 暗号化ファイルも解凍出来るようにしたい
ただしこれ全部をやるのは入門としては不適切なので実現難度を鑑みて実装するものを決めます。



実現可能か検証する


言語の構文などは後回しにしてまず実現可能かを調べます。
以下の2項目について調査します。
・解凍が出来るか?
・右クリックからどうやって実行するのか?


F#で解凍処理が出来るか?

一般的な処理ならば標準で用意されていたり、ライブラリが存在していることが多いと思います。
"F# 解凍処理"でググッてみます。
googleさんが親切に"C# 解凍処理"と解釈してくれます。(余計なことを......)

......見当たらないのでキーワードを変更します。
"F# zip"でググッてみます。

8番目くらいに以下の記事が表示されました。
http://furuya02.hatenablog.com/entry/20110904/1399766775

DotNetZipを使うという手があるようです。
ただしライセンスがMs-PLになっています。
少し脇道にそれていってますが気になるので調べます。

http://angelpinpoint.seesaa.net/article/413676920.html
https://msdn.microsoft.com/ja-jp/gg415737.aspx#P
http://blogs.msdn.com/b/shintak/archive/2012/09/09/10347542.aspx
http://kurusugawa.jp/2009/06/24/microsoft-shared-licenses/
DotNetZip を使用するにあたって
・使用に料金は発生しない
・DotNetZip自体をを変更しても公開する必要はない
・著作権、特許権、商標、またはその他の形式の出所を コード内に常に示しておく
と解釈しました。
権利の許可と条件と制限が矛盾しているように思えてなりません。
組み込んだソフトウェアやサービスなどは売り物にしても問題なし。
ソースコードとして配る場合はライセンス表示が必要と行ったところでしょうか?
デタラメ言ってる可能性もあるので裏はとっておいてくださいとお茶を濁しておきます。



と、ここまで調べておいてなんですがZipFileクラスというものが標準で用意されているようなのでそれを使います。
https://msdn.microsoft.com/ja-jp/library/system.io.compression.zipfile
ただし.NET Framework 4.5以降でパスワードの設定はできないようです。

F#の例がありませんがまあ恐らくいけるでしょう。




右クリックからどうやって実行するのか?

これはF#の話ではなくWindowsだとかVisualStudioの話になってくるでしょうね。主語を大きくして調べてみます。
"VisualStudio 右クリック 実行"で検索
これじゃダメそう

"アプリケーション 右クリック 実行"で検索
http://matome.naver.jp/odai/2135557366989247401
レジストリを弄る必要がありますね。ちょっと面倒ですね。

少し仕様を変えましょう。
ドラッグアンドドロップ(コマンドライン引数)でファイルを解凍できるようにしましょうか。
仕様変更はよくあることですね。はい。

とりあえず大体の流れはつかめましたね。
  • ドラッグアンドドロップ(コマンドライン引数)で受け取ったファイルを取得する
  • 右クリックメニューよりファイルを取得
  • ZipFileクラスを使って解凍処理
  • 暗号化されている場合パスワード入力画面を表示
  • 仕様にしたがって処理

ざっくりしすぎ?
ちなみにこの時点では私は関数型言語のことなど何も知りませんので間違っている可能性が高いです。
それは後々実装しながら修正していきます。





F#でHelloWorld


とりあえずHelloWorld作ってみますか
VisualStudio起動 → File → Project →
Visualstudio new project

ここでフォームアプリケーションが作成できないことに気づく(いや一応作れるけど)
F#はC#から簡単に呼び出せるようですしライブラリとして作ることを想定しているんでしょうかね?
コンソールアプリケーションで作っていくことにします。

恐らく以下の様なコードが初期表示されると思います。
[<entrypoint>]
let main argv = 
    printfn "%A" argv
    0 // return an integer exit code


初期状態では引数を受け取って文字を表示するようですね
見た感じ[<EntryPoint>]から実行されるのでしょう。
prinffnはcにおけるprintfとかと一緒でコンソール出力する関数でしょうね。
argvがコマンドライン引数
"%A"は文字列でしょうか?
最後の0はinteger型の戻り値だとコメントされていますね。


3行目を以下へ書き換えてみます。
printfn "Hello World"

F#でHelloWorld

HelloWorld完成
何も見らずにやるのはこの辺が限界ですね。
いい加減F#について勉強することにします。


F#でググッて出てきた以下をサラッと眺めます。
http://www.atmarkit.co.jp/fdotnet/special/introfs_01/introfs_01_01.html
http://fsharpintro.net/
https://msdn.microsoft.com/ja-jp/library/dd233181.aspx



...なるほど上記の
let main argv
はargvを引数に取るmain関数という意味なんですね。
letで変数、関数問わずに宣言できるようですね(束縛というらしい)
{}を使わずにインデントでスコープを表していますね。pythonみたいですね。
関数呼び出し時に括弧は不要のようです。(2つ以上だと必要?)
また現在引数argvは型推論を行っていますが、型を明確に指定することもできます。

少し修正してみましょうか


/// <summary>
/// <para>受け取ったファイルの解凍を行う</para>
/// <para>引数 filepaths : 解凍する複数ファイルのパス</para>
/// <para>戻り値</para>
/// <para>  true:成功</para>
/// <para>  false:失敗 </para>
/// </summary>
let unzip (filepaths : string[]) : bool =
    false

/// <summary>
/// エントリポイント
/// <para>引数 argv : コマンドライン引数</para>
/// <para>戻り値</para>
/// <para>  0:成功</para>
/// <para>  0以外:失敗 </para>
/// </summary>
/// </summary>
[<EntryPoint>]
let main (argv : string[]) : int =
    let filepaths :string[] = [| "test.zip" |]
    let canUnzip = unzip filepaths

    if canUnzip then
        printfn "解凍できました" 
        0
    else
        printfn "解凍できませんでした"
        -1



改めて自分のソース見るとコメント多いですね。

ざっくり説明します。
1~9行目で新たにunzipという関数を作っています。
その名の通りファイルを解凍する関数です。

関数宣言は使用する前にやらないといけないようです。
必然的にmainメソッドは一番最後ということになります。

///はドキュメントコメントと呼ばれるものになります。
関数使用時にインテリセンスが効くようになります。
VisualStudio インテリセンス
F#ではあまり対応していないようです(きちんと作っていればコメントは不要ってことですかね?)

引数と戻り値に型をつけています。右側にコロン型名で指定できるようです。
指定する必然性はないですがつけたほうがわかりやすいと思っているのでつけています。

21行目で解凍するファイルを変数に定義しています。
今は直接ファイル名を指定しているので後ほど変更します。
22行目にてファイル名を引数に解凍処理を行います。
canUnzipに成功したか失敗したかが返ってきます。
24行目で解凍結果を見てメッセージを表示しています。
特に説明することでもないですかね?
戻り値が少しわかりにくいですね。


とりあえず実行してみます。

アプリケーション実行


unzip関数はfalseを返しているだけなので今は必ず失敗します。
まずは同階層に存在するtest.zipを解凍するところからやることにしましょう。
unzip関数を作りこんでいきます。






実装


いよいよ実装に入っていきます。
基本的にわからない部分は都度調べながら実装します。


zip解凍処理を作成します。
References を右クリック → AddRefernce... → FrameworkからSystem.IO.CompressionとSystem.IO.Compression.FileSystemを参照するようにします。
この辺りはVisual Studio共通ですね。知らないとハマるかもしれません。

visualstudio 参照の追加

これでZip周りの機能が使用可能になったわけですが、どうやって使うんでしょうね?
適当にZipFileと打ってみますがインテリセンスには表示されません。
includeあるいはimportして使うのでしょうか?
"F# import"で検索をかけてみるとopenというキーワードがありました。

https://msdn.microsoft.com/ja-jp/library/dd393787.aspx

以下をファイル先頭に追記します。
open System.IO.Compression
openの記述がなくてもフルネームで指定すれば利用可能です。(System.IO.Compression.ZipFileなど)


ZipFileクラスの使用方法はmsdnのC#の使用例を見てJ#へとそれっぽく変換してみます。
https://msdn.microsoft.com/ja-jp/library/system.io.compression.ziparchive?cs-save-lang=1&cs-lang=csharp#code-snippet-3
C#のサンプルにUsingと言うキーワードがあります。ファイルなどを自動でクローズしてくれるので、リソースを取り扱うときに使用します。
F#ではusingの代わりにuseを使うようです。

open System.IO.Compression

/// <summary>
/// エントリポイント
/// <para>引数 argv : コマンドライン引数</para>
/// </summary>
/// </summary>
[<EntryPoint>]
let main (argv : string[]) : int =

    // 同階層のtest.zipをextractフォルダへ展開
    use archive:ZipArchive  = ZipFile.Open("test.zip", ZipArchiveMode.Update) 
    archive.ExtractToDirectory("extract")
    
    0


あれ?C#にしか見えない......
以上を実行するとbinフォルダ以下のtest.zipが解凍されてextractフォルダへと展開されます。意外と簡単。



当初の予定ではパスが存在しない場合にunzip関数はfalseを返そうと考えていたんですがZipFile.Openはパスがない場合、空のzipファイルを作成するようです。(しかしFileNotFoundExceptionを返すとドキュメントコメントに書いてあります。なぜ?)
そもそも失敗って何だ?どういう場合が失敗なんだ......
unzip関数の仕様が適当すぎたので再考します。
・解凍したzipファイル数を戻り値として返す
・存在しないファイルパスの場合は何もしない

OK、実装を進めていきます。


open System.IO.Compression

/// <summary>
/// <para>受け取ったzipファイルの解凍を行う</para>
/// <para>引数 filepaths : 解凍する複数ファイルのパス</para>
/// <para>戻り値  解凍したzipファイルの数</para>
/// </summary>
let unzip (filepaths : string[]) : int =

    for filepath: string in filepaths do
        use archive:ZipArchive  = ZipFile.Open( filepath, ZipArchiveMode.Update )  
        archive.ExtractToDirectory "extract"

    0

/// <summary>
/// エントリポイント
/// <para>引数 argv : コマンドライン引数</para>
/// </summary>
[<EntryPoint>]
let main (argv : string[]) : int =
    let filepaths :string[] = [| "test.zip" |]
    let extractPath:string = ""
    let extractCount = unzip(filepaths)

    printfn "%d個 解凍しました" extractCount

    0



それなりに形になってきました。
コメントも丁寧に修正しておきます。(メインのsummaryがおかしいことにここで気づく)

F#でファイルパスが存在するか調べる

ファイルパスが存在するか調べる方法を探します。
C#と互換性があるようなのでその辺りで探っていきます。
open System.IO を追記したらFileクラスが使えるようになりました。
File.Exists関数を使うことでファイルの存在が調べられそうです。

F#で値のカウント

F#は値が変更できないのでカウントの仕方がわかりません......
http://komorebikoboshi.hatenablog.com/entry/2014/12/19/100859
mutableキーワードで行けそうですね。
ただ++や+=演算子が使えません。恐らくそんなことするなってことでしょうね。
変更するときは = ではなくて <- を使うようですね。mutable(可変)な変数とimmutable(不変)な変数を分けているのでしょう。

let mutable count:int = 0
count <- count + 1


F#でZipファイルの情報を取得する

zipファイルの情報を取得する方法がしばらくわからなくて焦りました。
http://devlights.hatenablog.com/entry/20120826/p1
ZipArchiveクラスにEntriesプロパティがあるのでそれを利用します。


F#でコレクションを操作する

F#っぽさを出すためにコレクションを操作する方法を探します。
ここが一番時間かかってます。情報が少なすぎる。
カリー化って何だ?コンビネータってなんだ?
C#のlinqからどうにかキーワードに辿りつけました。
http://www.slideshare.net/Posaune/cerf-13021695
https://msdn.microsoft.com/ja-jp/library/dd233214.aspx
http://devadjust.exblog.jp/11077669/

このあたり何も知らない人が見たら意味不明だと思うのですがどうやって習得しているのでしょうね。本とか?

試行錯誤してF#っぽくしたソースが以下


use archive:ZipArchive = ZipFile.Open( filepath, ZipArchiveMode.Update) 
            
let archiveEntries : ZipArchiveEntry[] = [| for entry in archive.Entries -> entry |]

// archiveEntries が条件に一致するか調べる
// 条件:zipファイル直下がフォルダのみ
let pattern = Regex("/{\w}+")
let currentDirectoryExist : bool  = 
     Array.exists (
            fun (entry:ZipArchiveEntry) ->
                            pattern.IsMatch( entry.FullName ) = false && entry.Length = 0L
     ) archiveEntries



1行目でzipファイルを開いています。
useキーワードを使用しているので、スコープから外れたとき自動でファイルが閉じられます。

3行目でコレクションを変換しています。
コレクションにF#のFSharp.Collectionsと.netのSystem.Collectionsの2種類があるようですね。
ZipArchive.Entriesが.netのものでこれの変換方法を調べるのに手間取ってしまいました。

7行目は正規表現のためのクラスですね。C#から持ってきています。

8行目以下が条件に沿っているかを判定している部分です。
想定している仕様が直接ファイルが圧縮される場合zipファイル名で解凍、フォルダが圧縮されている場合そのまま解凍でした。
なのでzipファイル直下がフォルダのみかどうかを判定しています。
Array.exists ( ... ) archiveEntries
はコレクションarchiveEntries全てがカッコ内の条件を満たすかどうか調べるというものです。
ちなみに先程から名前が出ているコレクションは配列とかリストのことです念のため。


10行目はラムダ式を使う時の定型文ですね。
funはラムダ式を表すときに使います
entry:ZipArchiveEntryは変数宣言のようなものですかね。
ZipArchiveEntry型のentryということになります。
11行目で変数として使用することができます

11行目は条件式ですね。
archiveEntriesを先頭から走査していきます。


2階層下に存在するかフォルダならOK、それ以外の場合はNGになります。
最近よく見るわんらいなーってやつですね。
この辺りは説明見るより実際に書いたほうが理解しやすいです。

補足:コレクションって?

ここでは配列、リスト、マップなどのこと


バグ取りとエラー処理を行う


大体挙動は完成したのでバグ取りを行っていきます。。
関数の仕様が調べてもいまいちわからないので実際に動かしつつ処理を追加していきます。
確認する項目としては
・全てのパターンで正常に動作するか?
・へんなファイルを指定したらどうなるか?
・2回以上動かしたらどうなるか?
とかですかね?

ZipArchive.ExtractToDirectory メソッドは解凍先に同名ファイルがあると例外を吐くようなので例外処理をします。
メッセージだけだして処理を続けることにします。

try      
    archive.ExtractToDirectory( zipFilePath )
with // 解凍先に同名ファイルが存在した場合など
    | :? System.IO.IOException as ex -> printfn "例外エラー:%s" ex.Message
また例外が発生した場合アプリケーションが固まってしまうのでとりあえず全体をtry...withで囲っておきます。



完成!!



open System.IO.Compression
open System.IO
open System.Text.RegularExpressions
open System.Reflection

/// <summary>
/// <para>受け取ったzipファイルの解凍を行う</para>
/// <para>zipファイル直下がフォルダ一つの時はそのフォルダ名で解凍。それ以外はzipファイル名で解凍</para>
/// <para>引数 filepaths : 解凍する複数ファイルのパス</para>
/// <para>戻り値 解凍したzipファイルの数</para>
/// </summary>
let unzip (filepaths : string[]) : int =
    let tes = add 6 1
    let mutable extractCount:int = 0

    for filepath: string in filepaths do
        printfn "%s" filepath
        if File.Exists filepath then
            use archive:ZipArchive = ZipFile.Open( filepath, ZipArchiveMode.Update ) 
            
            // F#用に変換
            let archiveEntries : ZipArchiveEntry[] = [| for entry in archive.Entries -> entry |]
            
            // archiveEntries が条件に一致するものを取得
            // 条件:zipファイル直下がフォルダのみ
            let pattern = Regex("/{\w}+")
            let currentFolder  = 
                 Array.filter (
                        fun (entry:ZipArchiveEntry) -> 
                            pattern.IsMatch( entry.FullName ) = false && entry.Length = 0L
                 ) archiveEntries
            
            try      
                if currentFolder.Length = 1 then
                    // zipファイル直下が1つ and フォルダのとき 実行パスを解凍先へ
                    archive.ExtractToDirectory( Path.GetDirectoryName( Assembly.GetExecutingAssembly().Location ) )
                else
                    // それ以外 zipファイル名を解凍先名に
                    archive.ExtractToDirectory( Path.GetFileNameWithoutExtension(filepath) )
            with // 解凍先に同名ファイルが存在した場合など
                | :? System.IO.IOException as ex -> printfn "解凍エラー:%s" ex.Message

            extractCount <- extractCount + 1
            
    extractCount

/// <summary>
/// エントリポイント
/// <para>引数 filepaths : コマンドライン引数</para>
/// <para>戻り値 0:正常終了 -1:想定外のエラー(要するにバグ)</para>
/// </summary>
[<EntryPoint>]
let main (filepaths : string[]) : int =
    try      
        let extractPath:string = ""
        let extractCount = unzip(filepaths)

        printfn "%d個 解凍しました" extractCount

        0
    with
        | ex -> printfn "予期せぬエラー:%s" ex.Message; -1




まとめとか感じたこととか



F#はこんな言語だった!!

  • 戻り値が結構わかりにくい
  • C#などと比べてintellisenseが効かない
  • web上に情報が少ない
  • 検索結果にノイズが入りまくる(C#とかC#とかC#とか)
  • C#とF#は互換性あり
  • .net経験があれば習得は容易、ただしそれ以外は無理ゲー
  • なぜかJ#とミスタイプすることが多い(この記事を書くのに2桁はミスタイプした)


新しく言語を習得する場合

  • 1つか2つの言語に精通しておけば後はかなり楽
  • 言語が変わっても付け焼き刃だとあまり書き方は変わらない
  • プログラマは意外と直感に頼っている(私だけ?)
  • 少し考えてできないあるいは困難なときは別の切り口から考えてみる


githubにも上げてみました。
なんかミスっていたらごめんなさい。
https://github.com/ninomae-makoto/Unzip

まだまだ完璧には程遠いので、後は書籍をあさったり他人のソースを眺めつつ少しづつ自分のものにしていくといった感じでしょうか?
今回の記事はF#の機能はほぼ使っていないので注意してください。
そもそも私F#も関数型プログラミングもほとんど知らないですしね。
あくまで新しく言語を習得するときはどうするのかっていう記事です。あしからず

とまあいつもこんな感じで習得しています。

2015年12月31日木曜日