メインコンテンツへスキップ
← Back to Blog

【Haskell】head commandを自作した備忘録

haskell 自作

Motivation

今回はHaskellでLinux commandのheadを作成しました。

前回は、nlを実装したので今回は少しだけレベルアップした感じです、今回も前回と同様に自身で書いた後に、AI(Claude Code)にお手本を書いてもらい学習しました。

前回の記事は下記リンクから、見れるのでお読みいただけたら幸いです。

nlを自作した備忘録

自分で作成したhead command

{-# LANGUAGE OverloadedStrings #-}

module Main where

import qualified Data.Text as T
import qualified Data.Text.IO as TIO
import System.Environment (getArgs)

main :: IO ()
main = do
  args <- getArgs
  content <- case args of
    [path] -> TIO.readFile path
    _ -> TIO.getContents
  TIO.putStr $ myHead content 10

myHead :: T.Text -> Int -> T.Text
myHead text n = T.unlines . takeLine n $ T.lines text

takeLine :: Int -> [T.Text] -> [T.Text]
takeLine _ [] = []
takeLine n (t : ts)
  | n <= 0 = []
  | otherwise = t : takeLine (n - 1) ts

私は上記のように作成しました、takeLineはPreludeのtakeで代用できるなど、ツッコミどころ万歳ですが、そこはご愛嬌。

上記は、head -n 20 filePathなどの、オプションなどを作成できてなく、しかし思いつかなかったため、断念しています。

下記では、私のコードの改良点等あげていきます。

コマンドラインで可変長変数を作成する方法


data Options = Options
  { numLines :: Int           -- 表示する行数
  , filePath :: Maybe FilePath -- ファイルパス(Nothing なら stdin)
  }

defaultLines :: Int
defaultLines = 10

可変長変数を作成する前に、最初にOptionのデータ型を定義しておきます。defaultLinesに関してはheadはデフォルトでは10行表示するので、このようにしています。

headは以下のコマンドライン入力を受け付けるとします。

  • head
  • head file.text
  • head -n 10
  • head -n 10 file.text
parseArgs :: [String] -> Either String Options
parseArgs [] =
  Right $ Options defaultLines Nothing

parseArgs [arg]
  | "-" `isPrefixOf` arg = Left $ "unknown or incomplete option: " ++ arg
  | otherwise = Right $ Options defaultLines (Just arg)

parseArgs ("-n" : nStr : rest) =
  case reads nStr of
    [(n, "")] ->      -- -nの後の引数が10や46などであるか確認
      case rest of
        []     -> Right $ Options n Nothing
        [path] -> Right $ Options n (Just path)   -- その後にpathがあれば正しいコマンドとして処理
        _      -> Left "too many arguments" -- それ以上に引数があれば、エラー
    _ -> Left $ "invalid number after -n: " ++ nStr

parseArgs args =
  Left $ "invalid arguments: " ++ unwords args

parseArgsはコマンドライン引数のリスト受け取り、失敗であればStringを成功であればOptionsを返します。

引数が何もない場合は、Optionsにデフォルトの10行で処理する旨を2行めで書いています。

下記がエントリーポイントとコアロジックです。

main :: IO ()
main = do
  args <- getArgs
  case parseArgs args of
    Left errMsg -> do
      hPutStrLn stderr $ "head: " ++ errMsg
      hPutStrLn stderr "usage: head [-n lines] [file]"
      exitFailure
    Right opts -> do
      content <- case filePath opts of
        Just path -> TIO.readFile path
        Nothing   -> TIO.getContents
      TIO.putStr $ myHead content (numLines opts)

myHead :: T.Text -> Int -> T.Text
myHead text n = T.unlines . take n $ T.lines text

感想

まず、感想としてhaskellで可変長引数を作成するのが、本当に難しすぎ、最初に場合分けを明確にするべきだなと感じました。

head自体のコードの記述量は、そこまで多くなく拍子抜けでした。コマンドの処理のほうが多かった…

しばらくの間はLinux Commandを自作し続けて、haskellの感覚を身につけようと思います。