Haskell の lines 関数の代替

Haskell と Scheme の共通点 - satosystemsの日記のコメントで教えてもらいました。

linesWithLineEnd :: String -> [(String, String)]
linesWithLineEnd str = loop str []
    where
        loop [] [] = []
        loop [] prd = [(reverse prd, "")]
        loop cs@(c : cs') prd
            | take 2 cs == "\r\n" = (reverse prd, "\r\n") : loop (drop 2 cs) []
            | c == '\r' = (reverse prd, "\r") : loop cs' []
            | c == '\n' = (reverse prd, "\n") : loop cs' []
            | otherwise = loop cs' (c : prd)

main = print $ linesWithLineEnd "foo\r\nbar\rbaz\nfoobar"

⇒[("foo","\r\n"),("bar","\r"),("baz","\n"),("foobar","")]

僕が想定していた考え方とぜんぜん違う。

改行位置のインデックスを覚えておく、という方法ではなく、改行が出てくるまでの文字を別のリストにどんどん連結して再起呼び出しに渡し、改行が見つかったときにそのリストをいじる、という方法なら、確かにインデックスを覚えておく必要はないです。


同じロジックを Java で書くと以下のとおり。

import java.util.ArrayList;

public class LinesWithLineEnd {
    static ArrayList<String[]> linesWithLineEnd(String str) {
        ArrayList<String[]> list = new ArrayList<String[]>();
        loop(list, str, "");
        return list;
    }
    
    static void loop(ArrayList<String[]> list, String cs, String prd) {
        if (cs.equals("") && prd.equals("")) {
            return;
        } else if (cs.equals("")) {
            list.add(new String[] { reverse(prd), "" });
        } else {
            if (cs.startsWith("\r\n")) {
                list.add(new String[] { reverse(prd), "\r\n" });
                loop(list, cs.substring(2), "");
            } else if (cs.charAt(0) == '\r') {
                list.add(new String[] { reverse(prd), "\r" });
                loop(list, cs.substring(1), "");
            } else if (cs.charAt(0) == '\n') {
                list.add(new String[] { reverse(prd), "\n" });
                loop(list, cs.substring(1), "");
            } else {
                loop(list, cs.substring(1), "" + cs.charAt(0) + prd);
            }
        }
    }
    
    static String reverse(String s) {
        String result = "";
        for (char c : s.toCharArray()) {
            result = c + result;
        }
        return result;
    }
    
    public static void main(String[] args) {
        ArrayList<String[]> list = linesWithLineEnd("foo\r\nbar\rbaz\nfoobar");
        
        System.out.print("[");
        for (String[] s : list) {
            System.out.print("(\"" + s[0] + "\",\"" +
                (s[1].equals("\r\n") ? "\\r\\n" :
                s[1].equals("\r") ? "\\r" :
                s[1].equals("\n") ? "\\n" : "") +
                "\"),");
        }
        System.out.println("]");
    }
}

loop メソッドが再帰的に呼び出され、最終的には cs も prd も "" で呼び出されるので再帰が終わる、という実に関数型言語的なロジックです。

根本的にロジックを組み立てる考え方が違うので、関数型言語を使うときはリストで再帰、をしっかり考慮して考えなければならないですね。