LoginSignup
33
18

More than 3 years have passed since last update.

Elmでディレクトリ構造エディタを作る

Posted at

はじめに

動いているサイト
demo.gif
こんな感じの静的サイトを作ったという話です。(demoは少し古いですが)

全体的にMonocleやOptionalと言った概念を理解していないところが出ていると思うので、ご容赦ください。

使ったもの

実装

src/Main.elmに全てのロジックが書いてあります。なので上から解説していこうと思います。

model定義~init

Main.elm
-- MODEL

-- 階層構造的なID
type alias Id =
    List Int


-- 木構造のNodeに当たる部分, 今回valueはディレクトリ・ファイル名に相当
type alias NodeData =
    { id : Id, value : String }


-- Undo/Redo機能のためにUndoListで包んでいる
type alias Model =
    UndoList (Tree NodeData)


-- 最初は空
nodeTree : Tree NodeData
nodeTree =
    Tree { id = [], value = "" }
        []

-- 初期化
init : Model
init =
    UndoList.fresh nodeTree

model定義はelm-monocle Lens/Optionalで多分木操作を隠蔽を参考にしています。(いつもありがとうございます。)

更新処理(update)

Main.elm
-- UPDATE


type Msg
    = Change Id String
    | Add Id
    | Delete Id
    | Reset
    | Undo
    | Redo
    | Save
    | CancelSave


update : Msg -> Model -> Model
update msg model =
    case msg of
        -- idで指定したnodeの値を変更する。1文字ずつUndoしたくないのでここではUndoListを更新しない。
        Change id value ->
            { model | present = model.present |> Optional.modify (setNodeById id) (\nd -> { nd | value = value }) }

        -- idで指定したnodeを削除する(子供も含めて)
        Delete id ->
            case
                List.reverse id
                    |> List.head
            of
                Just index ->
                    UndoList.new
                        (model.present
                            -- 指定されたnodeを削除するためには、親の子供を変更する必要がある
                            |> Optional.modify (parentId id |> setChildrenById) (\children -> removeElement index children)
                            |> resetRootId
                        )
                        model

                Nothing ->
                    model

        -- idで指定したnodeの下に子を追加する
        Add id ->
            UndoList.new
                (model.present
                    |> Optional.modify (addTreeById id) (\_ -> Tree { id = [], value = "" } [])
                    |> resetRootId
                )
                model

        -- model(木)を初期化する
        Reset ->
            UndoList.new (Tree { id = [], value = "" } []) model

        Undo ->
            UndoList.undo model

        Redo ->
            UndoList.redo model

        -- focus時に状態を保存しておく
        Save ->
            UndoList.new model.present model

        -- blur時に何も変更がなければSaveをcancelする
        CancelSave ->
            if model.present == (UndoList.undo model).present then
                if UndoList.lengthPast model == 1 then
                    UndoList.fresh model.present

                else
                    UndoList.new (UndoList.undo model).present (UndoList.undo model |> UndoList.undo)

            else
                model

ポイントはOptional.modifyで、変更する対象+変更を与えると階層構造に対してよしなに変更してくれます。

Optionalをゴリゴリするところ

Main.elm
-- getterとsetterとIdを与えるとそれを変更できるものが返ってくる。
-- 自分でも何言ってるかわからないので、いい感じの解釈があれば教えて欲しいです。
getOptionalByGetterAndSetter : (Zipper a -> b) -> (b -> Zipper a -> Maybe (Zipper a)) -> Id -> Optional (Tree a) b
getOptionalByGetterAndSetter getFunc setFunc id =
    let
        goToZipper : Tree a -> Maybe (Zipper a)
        goToZipper tree =
            let
                goToNode zipper =
                    List.foldl (\idx mz -> mz |> Maybe.andThen (Zipper.goToChild idx)) (Just zipper) id
            in
            ( tree, [] ) |> goToNode

        -- 指定されたidのZipperを、dataで書き換える関数を用いて書き換え、Root Zipperに戻したもの
        replacedZipper tree data =
            goToZipper tree |> Maybe.andThen (setFunc data) |> Maybe.andThen Zipper.goToRoot

        -- 指定されたidのZipperからdataを得るgetter
        get tree =
            goToZipper tree |> Maybe.map getFunc

        -- 指定されたidのZipperのdataを書き換えるsetter, もし該当箇所が無ければ元のtreeを返す
        set data tree =
            Maybe.withDefault tree (Maybe.map treeOfZipper <| replacedZipper tree data)
    in
    Optional get set

-- Idを指定してTreeに子供を加えられるOptionalを返す
addTreeById : Id -> Optional (Tree a) (Tree a)
addTreeById =
    getOptionalByGetterAndSetter treeOfZipper Zipper.appendChild

-- Idを指定して子供を変更できるOptionalを返す
setChildrenById : Id -> Optional (Tree a) (Forest a)
setChildrenById =
    getOptionalByGetterAndSetter (treeOfZipper >> MultiwayTree.children) Zipper.updateChildren

-- Idを指定してNodeの値を変更できるOptionalを返す
setNodeById : Id -> Optional (Tree a) a
setNodeById =
    getOptionalByGetterAndSetter Zipper.datum Zipper.replaceDatum

getOptionalByGetterAndSetterを作ることで、子供を変更したり、値を変更したりするOptionalを簡単に作れるようになりました。

IdをResetする

Main.elm
resetRootId : Tree NodeData -> Tree NodeData
resetRootId tree =
    let
        -- 最初のIdを決めないと再帰できないので決めたのち、再帰関数(resetId)に渡す。
        newTree =
            Tree { id = [], value = (MultiwayTree.datum tree).value } (MultiwayTree.children tree)
    in
    resetId newTree


resetId : Tree NodeData -> Tree NodeData
resetId tree =
    let
        id =
            (MultiwayTree.datum tree).id

        newChildren =
            List.foldl
                (\child acc ->
                    let
                        -- 自分の直下の子供のIdは、自分のIdから決まる。
                        newChild =
                            Tree { id = id ++ [ List.length acc ], value = (MultiwayTree.datum child).value } (MultiwayTree.children child)
                    in
                    -- 子供のIdが決まったので孫のIdを再帰的に決めてもらう
                    acc ++ [ resetId newChild ]
                )
                []
                (MultiwayTree.children tree)
    in
    Tree (MultiwayTree.datum tree) newChildren

子供を加えたり、削除したりするとIdがめちゃくちゃになるので、そういう操作が入った時には毎回Idを初期化しています。(無駄な処理も多いので改善したい・ここでもOptionalを使えそう・・・?)

PlaneTextへの変換

Main.elm
tree2Plane : Tree NodeData -> String
tree2Plane tree =
    let
        -- 得られたディレクトリ構造のブロックの頭に、線(┣━ ・ ┃)を足す処理
        addHeader : String -> String
        addHeader s =
            case String.split "\n" s of
                first :: list ->
                    "\n├── " ++ first ++ List.foldl (\x acc -> acc ++ "\n│   " ++ x) "" list

                _ ->
                    ""

        -- 同じく線を足すが、最後尾は特別扱いしなければならない(┗━ を足す)
        addHeader4Last : String -> String
        addHeader4Last s =
            case String.split "\n" s of
                first :: list ->
                    "\n└── " ++ first ++ List.foldl (\x acc -> acc ++ "\n    " ++ x) "" list

                _ ->
                    ""
    in
    -- 最後尾を取りたいが、直接は取れないのでreverseしている
    case MultiwayTree.children tree |> List.reverse of
        last :: rest ->
            (MultiwayTree.datum tree).value
                ++ List.foldl
                    (\x acc ->
                        -- 子供のブロック(文字の塊)にHeaderを足して親に返していくという発想
                        acc ++ addHeader (tree2Plane x)
                    )
                    ""
                    (List.reverse rest)
                ++ addHeader4Last (tree2Plane last)

        _ ->
            (MultiwayTree.datum tree).value

acc ++ addHeader (tree2Plane x)の部分など(tree2Plane x)で完成したものが返ってくるという前提で組んでいくという、再帰関数的な思考に慣れるまで時間がかかりました。(小並感)

まとめ

Optionalの概念をまだ深く理解してはいないですが、不変なものの一部を変更するという観点に置いて非常に便利なものだとは理解できました。周辺ライブラリも色々とあるので、こういったものはElmでシュッと作れて非常に良いですね。

33
18
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
33
18