トップページへ

情報支援プログラムTjShiensection.hsの解説用文書

Access Count : 699

Copyright © 2019 TAKEHANA TADASHI
 配布、配信、公然での口述、販売、改変、をしないでください。

<TjShiensection.hsプログラムの解説用文書>

著作日時: 2020.08.31.月. 10:37:00 著作者、竹花 忠

※注.
 先頭に--が付いている行は、コメント行です。プログラムのコードではありません。
 また、{- と -} で、挟まれた・囲まれた・括られた、範囲もコメントです。プログラムのコードではありません。

※注.
 Haskell言語のソースプログラムのファイルタイプ名は、.hsにしてください。
 また、Haskell言語のソースプログラムは、UTF8のコード体系で保存してください。


準備作業:
 Haskell言語の開発環境のインストールについては、Haskell Platformをダウンロードします。
http://hackage.haskell.org/platform/から、使用しているOSにあったものをダウンロードしてください。
 ダウンロードしたらインストールしましょう。

 本プログラムは、フィルタープログラムとして作成してあります。
TjShiensection 検索対象ファイル名
とタイプ入力して改行で起動します。
 たとえば、検索対象ファイルの名前がsankakufile.txtであったなら、
TjShiensection sankakufile.txt
とタイプ入力して改行で、sankakufile.txtを検索対象ファイルとして、TjShiensectionが起動します。

 本プログラムを実行するには、Haskell言語の開発環境をインストールして、本プログラムを、コンパイル、リンク、してください。

 本プログラムを、コマンドプロンプトのカレントフォルダーに配置しておけば、コマンドプロンプトで、ghc TjShiensectionとタイプ入力してEnterキーを押すだけで、コンパイル、リンク、が完了します。

 コマンドプロンプトのカレントフォルダー以外にプログラムが配置してある場合は、プログラム名の前に、フォルダーを指定するパスの入力が必要になります。
 コマンドプロンプトのカレントフォルダーは、コマンドプロンプトのプロンプトの表示でわかります。コマンドプロンプトのプロンプトの表示が、C:\Users\Takehana>となっている場合、カレントフォルダーは、C:\Users\Takehanaです。つまり、Cドライブの下のUsersフォルダーの下のTakehanaフォルダーがカレントフォルダーである、ということです。
 そこに、プログラムのソースファイルを配置しておけば、GHC TjShiensectionをタイプ入力して、Enterキーを押すことで、コンパイル、リンク、が終了して実行ファイルが作成されます。
 実行ファイルを完成させてから、以下の使用方法の説明にしたがって、プログラムを起動してご使用ください。


使用方法:
 本プログラムは、コマンドプロンプトを起動して、実行します。
 コマンドプロンプトに、chcp 932を入力して、コードページをShiftJISに設定します。
 検索対象文章ファイルのコード体系は、ShiftJISでなければ、このプログラムは正常に動作しません。
 ただし、このプログラム自体は、UTF8のコード体系で作成してください。
 でもでも、検索対象ファイルは、ShiftJIS・ANSI、のコード体系で保存してください。
 本ソースプログラムを、コンパイル、リンクして、実行プログラムを作成してから、コマンドプロンプトから、TjShiensection 検索対象ファイル
を入力してEnterキーをタイプすると、本プログラムは起動します。


 参考書籍: すごいHaskellたのしく学ぼう!

-- 以下よりHaskell言語によるプログラムです。

import Data.List
import System.IO
import System.Directory
import System.Environment
import Control.Exception
import Control.Monad

{-概要・予備知識
 プログラム起動時、プログラム名の次の引数に、検索対象とするファイルの名前を指定します。
 "検索文字列:"のプロンプトに対して、検索対象とするファイル中の1セクションに含まれいる文字列1個をタイプ入力して改行します。
 すると、→行以下に、その文字列が含まれているセクションがすべて表示されます。
 なお、セクションとは、空行で隔てられた各範囲です。
 "検索文字列:"のプロンプトに対して、改行だけを入力すると本プログラムは終了します。
-}

main = do
  args <- getArgs
  let
    [targetFile] = args
    argcnt = length args
  if argcnt /= 1
    then
      putStrLn "使い方 : \nTjShiensection 検索対象ファイル名"
    else do
      contents <- readFile targetFile
--targetFileに格納されている名前のファイルから、検索対象コンテンツを取り込む。
      let
        lineContents = lines contents
        sectionContents = packSections lineContents
      pickupSectionLoop sectionContents

{-
 まずは、上記の関数mainから説明します。
 args <- getArgs、にて、コマンドライン引数の1つ1つを要素としたリスト、が取得されます。
 argsは、[String]です。

 [targetFile] = args、にて、argsリストの当該位置のデータが、左辺の変数に取得されます。
 targetFileは、Stringです。

 argcnt = length args、にて、コマンドラインで取得した引数の個数がargcntに取得されます。
 argcntは、Intです。

 if argcnt /= 1
   then
   else
によって、コマンドラインで取得した引数の個数が1個でなかった時の処理がthenの次に記述されます。
 また、コマンドラインで取得した引数の個数が1個であった時の処理がelseの次に記述されます。
 そして、thenの最終的な返り値の型とelseの最終的な返り値の型とは、同一の型である必要があります。
 このif argcnt /= 1
     then
     else
という文は、関数mainの最後の文なので、thenやelseのそれぞれ最後の関数の実行が終了した段階で、本プログラムは終了します。
 putStrLn "使い方 : \nTjShiensection 検索対象ファイル名"、は、引数の個数が1個でなかった時の処理です。エラーメッセージです。コマンドラインへの正しい入力の仕方を表示しています。そして本プログラムは終了します。
 引数の個数が1個だった時には、elseの後続の処理に進みます。
 contents <- readFile targetFile、にて、コマンドライン引数を格納しているtargetFileの内容を検索対象ファイル名として、そのファイルの全内容を、contentsに取得します。
 lineContents = lines contents、にて、その全内容を、1行ごとに1要素としたリスト形式のデータに変換したものを、lineContentsに取得します。
sectionContents = packSections lineContents、にて、セクションごとの内容を各1要素としたリスト、sectionContentsを取得します。
 pickupSectionLoop sectionContents、にて、先のリスト形式のデータを引数にして、関数pickupSectionLoopを呼び出します。
 以上で処理は終了です。つまり、pickupSectionLoopが終了してしまえば、本プログラムは終了します。
 ちなみに、ここで、thenの最終的な型はIO ()でした。elseの最終的な型もIO ()でした。
-}


-- 次に、下記の関数packSectionsを説明します。

--packSections lineContents
packSections :: [String] -> [String]
packSections [] = []
packSections lineContents@("":xs) =
  let
    (h,t) = break (/= "") lineContents
  in
    packSections t

packSections lineContents =
  let
    (h,t) = break (== "") lineContents
  in
    (unlines h) : packSections t

{-
 packSections [] = []、にて、packSectionsが空リストを引数にして呼び出された時には、[]・空リスト、を返します。

 次に、
packSections lineContents@("":xs) =
  let
    (h,t) = break (/= "") lineContents
  in
    packSections t
です。
 これは、関数packSectionsの引数lineContentsが、先頭要素が、""・空文字列、から始まるリストだった時の、定義・処理、です。
 まずbreakから説明します。
 関数breakは、第1引数に、述語とか述語関数とか呼ばれる、真/偽、を返す関数、第2引数にリスト、を設置して使用する関数です。
 なお、第1引数の、述語・述語関数、には、第2引数のリストの各要素が順番に供給されます。
 そして、第1引数は、述語・述語関数、による評価に基づいて、真か偽かを返します。
 関数breakは、述語・述語関数、が偽であり続けた間のリストの要素を、左辺のタプルの第1要素のリストに取得します。
 そして、述語・述語関数、が初めて真になった時から以降の要素を、左辺のタプルの第2要素のリストに取得します。
 つまり、関数breakは、述語・述語関数、が初めて真になった所よりも前の要素から成るリストとそれ以降のリストとに、第2引数のリストを分割して、それを左辺に取得する関数です。
 ですから、(h,t) = break (/= "") lineContentsによって、hには空文字列が連なったリストが取得され、tには空文字列が途切れてから以降のリストが取得されます。
 そして、packSections tにて、関数packSectionsを、先頭要素が、""・空文字列、以外から始まるリストを引数にして呼び出しています。
 その次は、
packSections lineContents =
  let
    (h,t) = break (== "") lineContents
  in
    (unlines h) : packSections t
です。
 これは、関数packSectionsの引数lineContentsが、空リストでもなく空文字列から始まるリストでもなかった時の、定義・処理、の記述です。
 (h,t) = break (== "") lineContentsにて、リストlineContentsを、空行が初めて検出された直前までのリストhと、それ以降のリストtに分割しています。
 そして、(unlines h) : packSections tにて、まず、空文字列の初めての検出によって分割された空文字列を含まない範囲のリストhを引数にして、関数unliensを呼び出しています。
 つまり、(unlines h)によって、リストhの各要素の後に改行文字が追加されてその全体を1個の文字列にします。
 それに、リスト構築子の:が続きます。つまり、(unlines h) : ですので、1個の文字列となった、1つのセクションを構成する(unlines h)が、リスト構築子によって新生されるリストの要素として登録されます。
 それに続いて、packSections tですので、関数packSectionsを、先頭が空文字列から始まるリストで呼び出しています。
 関数呼び出しの都度、該当する、定義・処理、の記述に置き換わることを繰り返して、そして、再帰呼び出しを含まない、定義・処理、の記述への置き換わりによって処理が終結します。
 以上によって、結局、関数packSectionsでは、リストlineContentsから、セクションを構成する要素をリストとして取り出しその各要素の末尾に改行文字を付加してその全体を1個の文字列としてセクションを構成します。この処理を、lineContentsの部分が空リストになるまで繰り返します。
 したがって、関数packSectionsは、各セクションを要素としたリストを、返り値とします。

 ちなみに、たとえば、本プログラムの引数である検索対象ファイルがdatafile.txtだとして、その内容が、
AB
D

FG


IJ
L
NO
とします。すると、本プログラムに基づきますと、
contentsは、['A','B','\n','D','\n','\n','F','G','\n','\n','\n','I','J','\n','L','\n','N','O','\n']
です。
lineContentsは、["AB","D","","FG","","","IJ","L","NO"]
です。
 そして、
sectionContentsは、["AB\nD\n","FG\n","IJ\nL\nNO\n"]
です。
 上記の内容の検索対象ファイルdatafile.txtが使用されて、本プログラムTjShiensectionが実行された時、関数mainの中の
sectionContents = packSections lineContentsでは、lineContentsが["AB","D","","FG","","","IJ","L","NO"]なので、パターンマッチングにより・形式の一致した、関数packSectionsの中の、
packSections lineContents =
  let
    (h,t) = break (== "") lineContents
  in
    (unlines h) : packSections t
が実行されます。
 ですから、(unlines ["AB","D"]) : packSections ["","FG","","","IJ","L","NO"]
となります。ここには、""で始まるリストを引数としてのpackSectionsの呼び出しが含まれているのでその部分では、今度は、パターンマッチングにより・形式の一致した、
packSections lineContents@("":xs) =
  let
    (h,t) = break (/= "") lineContents
  in
    packSections t
が実行されます。
 ですから、先ほどの(unlines ["AB","D"]) : packSections ["","FG","","","IJ","L","NO"]が、新たに、
(unlines ["AB","D"]) : packSections ["FG","","","IJ","L","NO"]
に置き換わります。ここには、""以外で始まるリストを引数としてのpackSectionsの呼び出しが含まれているのでその部分では、今回は、パターンマッチングにより・形式の一致した、
packSections lineContents =
  let
    (h,t) = break (== "") lineContents
  in
    (unlines h) : packSections t
が実行されます。
 ですから、先ほどの(unlines ["AB","D"]) : packSections ["FG","","","IJ","L","NO"]が今回は、
(unlines ["AB","D"]) : (unlines ["FG"]) : packSections ["","","IJ","L","NO"]
に置き換わります。ここには、""で始まるリストを引数としてのpackSectionsの呼び出しが含まれているのでその部分では、今度も、パターンマッチングにより・形式の一致した、
packSections lineContents@("":xs) =
  let
    (h,t) = break (/= "") lineContents
  in
    packSections t
が実行されます。
 ですから、先ほどの(unlines ["AB","D"]) : (unlines ["FG"]) : packSections ["","","IJ","L","NO"]が、新たに、
(unlines ["AB","D"]) : (unlines ["FG"]) : packSections ["IJ","L","NO"]
に置き換わります。ここには、""以外で始まるリストを引数としてのpackSectionsの呼び出しが含まれているのでその部分では、今回も、パターンマッチングにより・形式の一致した、
packSections lineContents =
  let
    (h,t) = break (== "") lineContents
  in
    (unlines h) : packSections t
が実行されます。
 ですから、先ほどの(unlines ["AB","D"]) : (unlines ["FG"]) : packSections ["IJ","L","NO"]が、新たに、
(unlines ["AB","D"]) : (unlines ["FG"]) : (unlines ["IJ","L","NO"]) : packSections []
に置き換わります。ここでついに、空リストを引数としてのpackSectionsの呼び出しが含まれているのでその部分では、パターンマッチングにより・形式の一致した、
packSections [] = []
が実行されます。
 ですから、先ほどの(unlines ["AB","D"]) : (unlines ["FG"]) : (unlines ["IJ","L","NO"]) : packSections []が、新たに、
(unlines ["AB","D"]) : (unlines ["FG"]) : (unlines ["IJ","L","NO"]) : []
に置き換わります。ここにはもう、関数呼び出しが含まれていないので、あとは、
この(unlines ["AB","D"]) : (unlines ["FG"]) : (unlines ["IJ","L","NO"]) : []の評価を進めるだけです。
 関数unlinesは、リストの各要素の末尾に改行文字を付加して、その全体を1個の文字列にします。
 したがって、結局、packSections lineContentsの呼び出しは、
["AB\nD\n","FG\n","IJ\nL\nNO\n"]
に置き換わります。
 よって、sectionContents = packSections lineContentsでは、sectionContentsに、["AB\nD\n","FG\n","IJ\nL\nNO\n"]
が取得されます。
 リストsectionContentsの各要素が、各セクションにあたります。
 つまり、"AB\nD\n"や"FG\n"や"IJ\nL\nNO\n"がそれぞれ別々のセクションです。
 この「セクション」は、本プログラムにおける用語です。「セクション」は、空行で隔てられた範囲の文字列のことです。
 関数packSectionsの実行様態は以上のようなものです。
-}


-- 次に、下記の関数pickupSectionLoopを説明します。

--pickupSectionLoop sectionContents
pickupSectionLoop :: [String] -> IO ()
pickupSectionLoop sectionContents = do
  putStrLn "検索文字列 : "
  searchString <- getLine
--標準入力から、検索文字列を取り込む。
  if null searchString
    then
      putStrLn "情報支援プログラム:TjShiensectionは、終了しました!!"
    else do
      let
        selectSections = filter (isInfixOf searchString) sectionContents
      putStrLn "→"
      mapM_ putStrLn selectSections
      putStrLn ""
      pickupSectionLoop sectionContents

{-
 putStrLn "検索文字列 : "、にて、検索文字列の入力を促すプロンプト"検索文字列 : "、を表示します。

 searchString <- getLine、にて、タイプ入力された改行までのうち、改行を除いた文字列がsearchStringに取得されます。
 searchStringの型は、Stringです。

 if null searchString
   then
   else
によって、searchStringの中身が空だった時、thenの次からが実行されます。
 また、searchStringの中身が空でなかった時、elseの次からが実行されます。
 thenの最終的な型とelseの最終的な型は同一の型でなければなりません。
 このif null searchString
     then
     else
という文は、関数pickupSectionLoopの最後の文なので、thenやelseのそれぞれ最後の関数の実行が終了した段階で、本プログラムは終了します。
 putStrLn "情報支援プログラム:TjShiensectionは、終了しました!!"、は、searchStringが空だった時に実行されます。"情報支援プログラム:TjShiensectionは、終了しました!!" 、と表示して本プログラムは終了します。これは、本プログラムの通常の終了の仕方です。
 searchStringが空でなかった時には、elseの後続の処理に進みます。

 selectSections = filter (isInfixOf searchString) sectionContents、にて、sectionContentsの、各1要素・各1セクション、のデータの中に、searchStringに取得されている検索文字列が含まれていれば、その1セクションを・その1要素を、追加してリストselectSectionsを作ります。
 詳細に説明すると次のようなことです。
 関数filterは、第1引数に、述語とか述語関数とか呼ばれる、真/偽、を返す関数、第2引数にリスト、を設置して使用する関数です。
 なお、第1引数の、述語・述語関数、には、第2引数のリストの各要素が順番に供給されます。
 そして、第1引数は、述語・述語関数、による評価に基づいて、真か偽かを返します。
 関数filterは、述語・述語関数、が真を返した時の、述語・述語関数、に供給されたリストの要素をリストアップします。
 filter (isInfixOf searchString) sectionContents、はその一例です。
 ここで、関数isInfixOfは、その第1引数が、その第2引数の中に含まれていた時に真を返し、それ以外の時に偽を返す、関数です。
 ここでは、その関数isInfixOfに第1引数が適用してあるので、つまり、(isInfixOf searchString)となっているので、(isInfixOf searchString)全体で、引数1個を要求する関数、として扱われます・となっています。
 (isInfixOf searchString)の引数として文字列が供給されると、その文字列の中に、変数searchStringに格納されている文字列が含まれていた時に真を返し、それ以外の時に偽を返します。
 そしてここで変数sectionContentsは、各要素が各1セクションの文字列から成るリストです。
 なので、関数filterの機能により、(isInfixOf searchString)に毎回、lineContentsから順番に各1行の文字列が供給されます。
 そして、(isInfixOf searchString)が真を返した時の1行の文字列が、関数filterの機能により、リストアップされてゆきます。
 したがって、filter (isInfixOf searchString) sectionContents、によって、変数searchStringに格納されている文字列を含んでいる、リストsectionContentsの要素をリストアップしたリストが取得されます。
 なお、関数filterの第1引数である、述語・述語関数、は、1引数の関数であることが要求されます。
 関数isInfixOfは2引数関数ですが、そのうちの引数1個を、適用して・部分適用して、(isInfixOf searchString)とすることで、残り1個の引数だけを要求する1引数関数、となっています・の扱いとなります。
 ですから、filter (isInfixOf searchString) sectionContents、は正常に動作します。

 putStrLn "→"、にて、"→"を表示します。

 mapM_ putStrLn selectLines、にて、検索文字列を含んでいた行を要素とするリストselectLinesを、1要素ずつ表示し尽くします。
 ここでは、関数putStrLnによる画面へのリストデータの表示だけが目的であって、その結果の返り値である()を取得することは目的ではありません。
 なので、返り値は捨ててしまうことになるmapM_を使います。
 mapM_ではなく、mapMを使えば、返り値の取得も行われます。
 rval = mapM putStrLn ["ab", "cd", "ef"]、なら、rvalに[(), (), ()]が取得されます。
 mapM_を使用した場合には、同様の記述にしても、つまり、
 rtval = mapM_ putStrLn ["ab", "cd", "ef"]、としても、rtvalは、()となるだけで、putStrLn "ab", putStrLn "cd", putStrLn "ef"、のそれぞれを実行した返り値のリストアップは行われません。
 mapMにしてもmapM_にしても、共通なのは、第1引数の関数を、第2引数のリストの各要素に割り当てて形成される関数呼び出しのリストを、順次、実行すること。
 mapMとmapM_の違いは、mapMは、関数呼び出しのリストを実行したあとの返り値をリストアップするが、mapM_は、関数呼び出しのリストを実行したあとの返り値を捨て去ってしまうことです。
 ちなみに、mapM putStrLn ["ab", "cd", "ef"]、は、sequence [putStrLn "ab", putStrLn "cd", putStrLn "ef"]、と同等です。

 putStrLn ""、にて、セパレータとして1行、空行を表示します。

 pickupSectionLoopを、またsectionContentsを引数にして呼び出します。

 thenの最終的な型は、putStrLnの返り値の型のIO ()です。
 elseの最終的な型は、pickupSectionLoopの返り値の型のIO ()です。
 thenもelseも最終的な型が同一だったので、このif then else文は、正常に動作します。

 ですから、本プログラムは、"検索文字列 : "、のプロンプトに対して、改行だけを入力した時に終了し、それ以外の入力の時には、検索結果を表示しては再び、"検索文字列 : "、のプロンプトを表示して、待機状態になることを繰り返します。
 以上が本プログラムが行っていることです。
-}


 プログラムの設計構想の骨子については、情報支援プログラムTjShiensection.hsの設計構想の骨子を参照してください。