一方、Haskellチュートリアル

この記事では、一般的な複雑さと高度に専門化された関数型プログラミング、特にHaskell言語に関する神話を暴きたいと思います。 Haskellの理解が最小限の人でも、この記事を理解できるようにしようと思います。 しかし、最初に、小さな紹介。

自分は怠け者のアマチュア写真家だと思います。 私には良い「ミラーレスミラー」があります。時々、自分の周りの何かをクリックしたいという欲求に襲われます。 しかし、私は怠け者であり、結果の写真アーカイブをいじくり回し、時間も欲望もありません。 原則として、カメラをHDMIケーブルでテレビに接続することで、撮影後すぐに1、2回写真を見ることができます。 その後、写真は忘却に送られます〜/ Pictures / Photos / Unsortedディレクトリ、そして、原則として、永遠にそこに残ります。 さまざまなスペシャル付き。 私はどういうわけか友達を作らなかったので、この混乱はほぼ2年続いた。 そして、Haskellを研究した結果、私は問題を解決するために成熟しました。

免責事項


私は関数型プログラミングの第一人者のふりをしません。私が書いたコードはひどいものだと認めています(結局、Haskellは暇なときに2か月以上間密接にやっています)。さらに、正しく動作しない可能性があります。 この記事の目的は、Haskellが悪夢や複雑な数学的計算だけでなく、日常的な日常的なタスクにも使用できる汎用言語であることを示すことです。 そしてそれらに完全に対処する。

この問題を解決する特別なプログラムが存在することは承知していますが、必要なことを正確に行うシンプルで基本的なユーティリティが必要でした。 この問題は一般的にいくつかのbashまたはperlで解決できることを知っていますが、これは著者、つまり私が選択したものです。

始める前に


これは長引く導入の最後の部分だと思います。 最初に、もちろん、Haskellの基本的な知識を持っていることが望ましいです。さもないと、多くのことがわかりにくいかもしれません。 モナドのアイデアがあるといいですね。 ただし、プレゼンテーションのプロセスにおける「微妙な」点について説明するようにします。この記事を皆さんが少しでも努力して理解してくれることを願っています。

第二に、必要なツール。 さらに、すべてのイベントはLinuxマシン上で展開されるものと想定されていますが、マイナーな変更を加えると、Windows、さらにはMac OS Xでも同様になります。
  1. Haskellプラットフォームをディストリビューションのリポジトリからダウンロードしてインストールします。
  2. EXIFを使用するHaskellライブラリをsudo cabal install --global exif ;
  3. GTK + \ Gladeおよび対応するHaskellのバインディングをsudo cabal install --global gladesudo cabal install --global glade


問題の声明


そのため、対応するフォルダーに日付ごとに写真を「パック」するための最も簡単なユーティリティが必要でした。 原則として、すべて、複雑なことは何もありません。

どのように決定しますか


すべてがシンプルです。 まず、2つのモードをサポートする必要があります。
一般的に言って、GUIはここではまったく必要ありませんが、これはHaskellのGUIも決して怖くないことを示す一般的な小さなトレーニング例であることを思い出してください。

さらに、カタログがどのようにスリップされたとしても、多くのカメラ(すべてではないにしても)がPHOTOフォルダーに慎重に写真を入れてから、100PANA、101PANAなどにこのディレクトリをバイパスします(すべてのサブディレクトリをバイパスすることを含む)。 .p。)各ファイルを処理します。

個々のファイルの処理は非常に簡単です。Exifデータを読み取り、もしあれば、抽出した日付に対応するディレクトリにそれ(写真ファイル)をコピーします。

望んでもいなくても、プログラムが何をすべきかを伝える過程で、その3つの主な機能を説明しました。


ここでいくつかのソースコードを追加しましょう。

コードにより近い


叙情的な余談。 Haskellプログラムは、Cのようにモジュール構造を持っていますが、もちろんひどいヘッダーファイルはありません。 エクスポートされた(外部)関数はモジュールの説明で指定され、他のモジュールとコードのインポートがあります。 Haskellプログラムは、データ型と関数の説明のみで構成されています。 それだけです

それでは、もう一度始めましょう。 つまり、Mainモジュールのmain関数を使用します(結局、プログラムを開始するのはそれです)。 これがモジュール全体のコードです
メイン:

  1. モジュールメイン
  2. インポートシステム(getArgs)
  3. 輸入マネージャー
  4. インポートGUI
  5. メイン:: IO()
  6. main = getArgs >> =起動
  7. -写真へのパスを含む単一の引数が必要です。それ以外の場合はguiを実行します
  8. 起動:: [文字列]-> IO()
  9. launch [x] = processDirectory x
  10. launch _ = startGUI
*このソースコードは、 ソースコードハイライターで強調表示されました。


最初のモジュールの説明をもう少し詳しく説明し、主なものを説明します。 したがって、最初の行はモジュールの説明です。 次はインポート行です(getArgs関数のみがSystemモジュールからインポートされ、実行可能ファイルの名前なしでコマンドライン引数のリストを返します)。 マネージャーおよびGUIモジュールについては後述します。

以下はmain機能の説明です。 説明の最初の行は、タイプのオプションの定義であり、一般的には、必ずしも必要ではありません。ほとんどの場合、関数のタイプは、提供された引数のタイプから推測できます。 関数は方程式系で記述され、2行目はその方程式です。

叙情的な余談。 正式には、すべての機能はクリーンで「ダーティ」に分類でき、副作用があります。 私は指で説明しようとします。
関数は、同じ入力データで同じ出力データを生成し、システムの状態を変更せず、それらに依存しない(ファイルシステムの状態、または開いているTCP接続の存在)場合、クリーンです。 例として追加機能を取り上げます。 2つの引数を取り、これらの引数の両方が数値2に等しい場合、出力では常に(絶対に常に)数値4を取得します。そして、はい、TCP接続を追加する機能は開きません。
副作用のある関数は別の会話です。これらは、 入力/出力モナド(IOモナド)と呼ばれるモデルを介して、外部の兆候に対する関数の依存関係を示すために考案されたものです。 認識を簡単にするために、現時点では「入力/出力」のコンテキストでのみモナドについて考えます(実際、この概念ははるかに広いです)。 副作用のある関数は、 アクションのシーケンスの記述であり、その結果、特定の結果値を取得します。 例:
readFile :: FilePath -> IO (String)は、このファイルへの指定されたパスにあるファイルを読み取る関数です。 より正確には、これは関数ではなく、そのタイプです。 関数のタイプを読むのは非常に簡単です。最初に関数の名前が来ます。次に、記号が付いた2つのコロンの後に->関数の引数が分離され、極端な後に->戻り値のタイプがあります。 これは基本的な説明ですが、今のところはこれで十分です。 つまり、関数が特定のFilePathを受け入れ、入力/出力アクションを返し、その実行により文字列値が得られることがわかります。
アクションのシーケンスを理解することが重要です!=計算のシーケンス。 実際、 通常 、計算が実際に行われる順序は正確はわかりません。計算の遅れを決定するだけです。 したがって、モナドなどの抽象化の存在は、Haskellを必須にするものではありません。

したがって、 mainに戻ります。 引数を取らず、明らかにI / Oアクションを返します。 このアクションの実装は、プログラム全体の実行です。 おもしろいですが、受け取ったプログラム全体に単一の純粋な関数が書かれているわけではなく、利用可能なすべてのチュートリアルとはまったく異なりますが、これはプログラムが解決する問題です。 アクションのシーケンスは、2つの引数を取る関数>>=および>>によって決定されます。 最初のケースでは、左側のアクションの結果が引数として右側の関数呼び出しに渡され、2番目のケースでは、左側のアクションが単にサイレントに実行され、その後右側のアクションが実行されます。

この場合、 getArgs関数で定義されたアクションを実行すると、アプリケーション引数文字列のリストが実行され、それがlaunch関数に渡されます。 起動機能について以下に説明します。 引数として文字列のリストを受け取りますが、値を返さないI / Oアクションも返します。 既に述べたように、Haskellの関数は方程式のシステムによって記述され、それは起動の定義で視覚的に提示されます。

叙情的な余談。 特定の関数呼び出しごとにどの式が選択されるかは、パターンマッチング手順、つまりパターンマッチングによって決まります。 関数テンプレートは、左側で定義された方程式の一部によって定義されます。 パターンマッチング手順は、コードに表示される順序でコントロールを選択します。 「何でも」の普遍的なテンプレートはシンボル_です。 テンプレートでは、さまざまなデータ構造を分解できます(たとえば、そうする必要があります)。たとえば、 foo ["a",""] = 0と書くと、関数fooに2行 "a"と "b"のリストが来ると、 0を返します。

したがって、 launch関数に単一の値xリストが付属している場合、これはディレクトリへのパスであると考え、その自体に等しい引数を使用してManagerモジュールからprocessDirectory関数を呼び出します(関数をどのモジュールから明示的に示す必要はないことに注意してくださいこれが二重の解釈を与えない場合、つまり、2つのモジュールに同じ名前の関数があります)。 そして、何か他のもの( _テンプレートに注意を払う) _を実行します。

これがメインモジュール全体です! プログラムの機能1を実装しました。それを開始しました!
はい、私はそれがどれだけひどく未開始を探しているか理解していますが、今はソースモジュールMainをもう一度見てください。 すべての叙情的な余談をもう一度読んで、自分に言い聞かせてください、それはとても難しいですか? たぶん異常なのでしょうか? すべてのテストに勇敢かつストイックに合格した人のために、読み続けることを提案します。

EXIFデータ


ここではメインの計算プロセスを終了し、EXIFから写真の時間データを抽出する機能を記述しましょう。 このために、別のPhotoモジュールが作成されました。 なぜ分離するのですか? それは私の本能によって私に指示されたので、これは写真を扱うためのモジュールであると想定されています、突然私のソフトウェアは追加機能で成長しますか? そのソースを見てみましょう。

  1. モジュール写真(
  2. getTime
  3. ここで
  4. インポートデータ時間
  5. System.Localeのインポート
  6. Graphics.Exifをインポートする
  7. getTime :: String-> IO(たぶん
  8. getTime filePath =
  9. fromFile filePath >> =(\ e-> getTag e "DateTime")>> =(return。parseDateTime)
  10. どこで
  11. parseDateTime(ちょうどstr)= parseTime defaultTimeLocale "%Y:%m:%d%H:%M:%S" str
  12. parseDateTime Nothing = Nothing
*このソースコードは、 ソースコードハイライターで強調表示されました。


怖い まったくありません。 今回はモジュール定義で、括弧内にgetTime関数の名前を指定したことに注意してください。 モジュールの外部からアクセスできる機能はここに示されています。 C \ C ++のフラッシュヘッダーファイルの代わりに、シンプルでエレガントなソリューションを紹介します。

getTime関数に注意してください。 引数としてファイルパスを取り、 Dayになる可能性のあるアクションを返します。 多分それは何らかのキーワードではなく、2つの値を取ることができる非常に明確なタイプです:Just SomethingまたはNothing。 結果として、関数が値の範囲から特定の値を返さない場合、このデータ型が使用されます。 たとえば、指定された条件に一致するリスト内のアイテムを検索します。 そのような要素はそうではないかもしれません。

ここで、Maybeの存在は、このファイルに時間データがない可能性があることを示しています。 日は、Haskellの時間を表すデータの一種です。 getTime関数の定義(つまり、方程式の右側)は、私たちにとって新しい概念をさらに2つ紹介します。 しかし、まず最初に。 getTimeは3つのアクションで構成されていることがわかります。

まず、怖くて物議を醸す言葉が戻ってきます。 実際、Haskellに戻っても何も返されず、関数の定義を非常に間接的に参照します。 returnは、渡された値をactionに変換する単純な関数です。 実際には、実行がアクションのチェーンの実行である関数は、「純粋な」値を返すことができません。 このストリングを取得することは特定のアクションチェーンを実行することによってのみ可能であるため、「単なるストリング」を返すことはできません。つまり、このストリング自体を取得することはアクションもあるため、結果の値をモナドにラップする必要があります。 「返品」(返品)を受け取った場所に。 これはモナドから取得されたもので、それをモナドに返す(ラップする)必要があります。 戻り値に関して:モナド関数は、その中で定義された最後のアクション、つまり この場合、ただ戻ります。
第二に、ドット記号は機能の組み合わせです。 fy = z; gx = y; fy = z; gx = y; それから(f . g) x == f( gx ) == z 。 2番目のアクションの結果として、日付を持つ文字列を取得し、関数自体がDayを返す場合があります 。 したがって、この文字列を最初にparsimにすることができます。その結果、明らかに、日付がある可能性があることが判明し、それを返します。 実際、最後のアクションは次のように記述できます。
\ str -> return (parseDateTime str)


最後に、whereブロック。getTimeの定義で使用したparseDateTimeを説明します。 whereブロックは、たとえばコンテキスト関数を定義します。 それらの範囲は、このブロックが発生する特定の定義によって制限されます。 これらのコンテキスト関数では、親関数の入力引数にアクセスできますし、アクセスする必要があります。 この場合、このようなコンテキストの関数は、値を解析する関数です。 Just and Nothingを使用して、2つのテンプレートの2つの式で表現する必要があるため(別の方法では、その定義をgetTimeの定義に直接代入することが可能)、別のブロックに配置します。 最初のケースでは値を解析し、2番目のケースでは再びNothingを返します。 同意します、はっきりと?

時間抽出機能は終了しました。 ディレクトリをバイパスできます。

機能2-ディレクトリトラバーサル



叙情的な余談。 Haskellは、命令型言語の問題解決に慣れている方法ではなく、少し違った考え方をさせます。 たとえば、ファイルがあるディレクトリを巡回する場合、Cプログラマーは次のように言います。「ループでファイルを調べてAをチェックし、Bを実行します」。 ただし、関数型言語のプログラマーは、「ディレクトリ内の各ファイルに処理機能を適用します」と言います。

あなたの思考を少し再構築すると、失われたサイクルを後悔するのをやめます。 関数型言語では、それらは畳み込みと「マップ」によって正常に置き換えられます。 ここでは畳み込みは必要ありません。説明に煩わされることはありません。 マップ機能について説明します。 意味で最も近いロシア語は「マッピング」であり、これは実際にそうです。 map関数は、あるデータセットを別のデータセットにマップします。 入力として、タイプaの要素のリスト、タイプa-> bの関数(そして、どのように関数型言語であるか、ここで関数を引数として渡すことができます!)を受け取り、最後にタイプbのリストを取得します。 マップ関数のタイプは次のとおりです。

map :: (a -> b) -> [a] -> [b]


最初の引数の括弧に注意してください。2番目の引数は関数そのものであると報告されます。 一般的に、Haskellでは、すべての関数は実際には単一の引数の関数であり、結果として、それらのいくつかは値ではなく関数も返します。 追加機能を検討してください。

add :: Int -> Int -> Int


そのタイプは、次のように正しく記述する必要があります。

add :: (Int -> (Int -> Int))


違いを感じますか? したがって、 add 5 2は値7を返します。ただし、 add 5 、Int型の値を取り、Int型の値を返す関数を返します。 このような奇跡に基づいて、マップ機能を使用する原理が構築されています。 これは、元のリストのすべての要素に1を追加する方法です。

map (+ 1) [1,2,3]


しかし、Haskellのチュートリアルで同様の例を見つけることができます。 羊に戻りましょう。 カタログ内の要素のリスト全体で、完全に同一のアクションを実行し、これらのアクションの結果のリストを取得する必要があることが明らかになりました(実際、それは少し間違っていますが、問題ではありません)。 感じますか? これは、マップの適用が要求される場所です。 ただし、最初に、カタログの各要素で何を行う必要があるかを判断します。



次に、元のコードの一部を意図的に削除し(または「擬似コード」プラグに置き換えて)、最も重要なコードのみを残します。 これを省略しながら、このレコードがディレクトリであるかどうかについて何らかの情報を見つけたと想像してください。 次に、次の処理機能を検討します。

  1. -要素の処理:カタログの場合-再帰を入力、それ以外の場合は写真を処理します
  2. processSingle ::( String 、Bool)-> IO()
  3. processSingle(path、True)= processDirectoryパス
  4. processSingle(パス、False)= do
  5. picturesDir <-getPicturesDir
  6. maybeDate <-getTimeパス
  7. copyPhoto picturesDir maybeDate
  8. どこで
  9. -安全なコピー
  10. copyPhoto pictures Nothing = return ()
  11. copyPhoto pictures(日付のみ)= do
  12. let NewPath = pictures ++ "/" ++(formatTime defaultTimeLocale "%Y /%B /%d"日付)
  13. copyFileパスnewPath
*このソースコードは、 ソースコードハイライターで強調表示されました。


そこで、関数processSingleを取得しました。 引数として、2つの要素のタプル(つまり、値のペア)を取ります:ファイルシステム要素へのパスと、それがディレクトリであることのサイン。 パターンマッチングを使用して、関数を2つの方程式に分割しました。1つ目はディレクトリ用、2つ目は再帰用、2つ目はファイル用です。 ここでは、アクションの記法について最初に紹介します。

叙情的な余談。 その瞬間まで、特別なアイコンで区切られた一連のアクションを「次々に」記録しました。 これは、たとえば、アクションから抽出された値のテンプレートと「オンザフライ」で比較する必要がある場合など、必ずしも便利ではありません。 または、より頻繁に起こることとして、2つのアクションを実行し、それらの結果を3番目の関数に転送する必要があります。 ここで、do-notationが助けになります。これは、実際には初心者の目に優しいです。 以下に2つの同等のコードを示します。

  1. foob​​ar = action1 >> = action2 >> = action3
  2. foob​​ar '= do
  3. result1 <-action1
  4. result2 <-action2 result1
  5. action3 result2
*このソースコードは、 ソースコードハイライターで強調表示されました。


この場合、2つの異なるアクションの2つの結果のみが、3番目の関数の引数として使用されます。 コピー機能は簡単です。日付が見つかったら、テンプレートで再度照合し、ファイルをpathからnewPathコピーします。 2つの機能に注意してください。

formatTimeは、明らかに、ロケール、パターン、および日付を受け取り、そのパターン用にフォーマットされた日付の文字列を返す関数です。
++はリスト連結関数です。

突然、関数#3について説明しました。 本当に簡単ですか? したがって、関数番号2はどのようになりますか。 次のようになります。

  1. processDirectory :: 文字列 -> IO()
  2. processDirectory dir =
  3. getDirectoryContents dir >> = checkItems >> =(mapM_ processSingle)
  4. どこで
  5. -ディレクトリの内容の特定のリストについて、各要素のマーカー「ディレクトリ」を含むタプルを返します
  6. checkItems xs = mapM singleCheck xs
  7. どこで
  8. singleCheck path = do
  9. isDirectory <-(doesDirectoryExistパス)
  10. return (パス、isDirectory)
*このソースコードは、 ソースコードハイライターで強調表示されました。


これをよく見てください。実際、これは非常に単純な関数です。しかし、最初に、mapMとmapM_について少し説明します。

叙情的な余談。上記で説明したmap関数は、クリーンなコードでのみ機能します。コードはモナドですので、モナドマップを使用する必要があります。これらの関数のタイプを考慮してください。
mapM :: (a -> IO b) -> [a] -> IO [b]
mapM_ :: (a -> IO b) -> [a] -> IO ()


それらは純粋なマップとまったく同じ意味を持ちますが、出力では純粋な値ではなく、これらの値を返すアクションを提供します。最初のケースでは、特定の各値を処理した結果に関心があり、処理された値のリストを生成するアクションを取得します。2番目のケースでは、処理の結果に関心がなく、アクションを完了する必要があります。

したがって、コードを下から上に読みます。

そしてそれだけです!おわり!プログラムの準備ができました。最後の部分の前に、GUIについて少し説明したいと思います。

宣言型言語のGUI


宣言型言語と命令型言語の違いは何ですか?命令型言語では、計算を行う方法を記述する必要があり、宣言型言語では、受け取りたいものを記述する必要があります。宣言型言語の最も明確な例はSQLです。ここで、グラフィカルインターフェイスについて少し考えてみましょう。実際、(特定のツールキットの規則に従って)宣言的に表示する必要があるものを記述、オブジェクトと対話するときにオブジェクトがすべきことをすぐに記述する必要があります。

Haskellに戻りましょう-機能的な宣言型言語として、この構造に素晴らしく配置されています。たとえば、ボタンクリックハンドラーを作成するには、引数としてハンドラー関数を渡すだけで十分です。そしてそれはとても自然です。Haskellでインターフェースを作成するには、GTKを使用します。これは次のように行われます:Gladeでフォームを描画し、Haskellコードでハンドラーを配置します。

タスクの簡略化されたコードは次のとおりです。

  1. prepareGUI mainWindow startButton fileChooser =
  2. する
  3. onDestroy mainWindow mainQuit
  4. onClicked startButton(processClick fileChooser)
  5. どこで
  6. processClick fileChooser = fileChooserGetFilename fileChooser >> = processDirectory
*このソースコードは、 ソースコードハイライターで強調表示されました。


彼はコメントを必要としないと思う。

おわりに


プロジェクトの完全なソースをgithub(下のリンク)に投稿しました。興味のある人は皆知り合うことをお勧めします。かなり詳細なコメントを提供しました。結果として得られた記事の印象的な量にもかかわらず、私はそれを注意深く読んだ人があなたのHaskellがそれほど複雑ではなく、ほんの少し異なることを理解したと信じています。それは他の原則に基づいていますが、それらの基礎に基づいて、それがシステムまたはアプリケーションのプログラミングであるかどうかにかかわらず、任意のレベルでコードを書くことはかなり可能です。さらに先へ進むことができます:少しの間、Haskellに基づいて素晴らしいMVC Webフレームワークが生まれることを想像してください!このチュートリアルでは、氷山の一角に触れただけで、実際にはまだ学ぶべきことがあります-ポリモーフィズム、型クラス、並列処理!..この記事を読んだ人が、少なくともこの美しい言語を学びたいという欲求を呼び起こしたことを願っています。

Githubプロジェクト
Haskell

Source: https://habr.com/ru/post/J116760/


All Articles