5
\$\begingroup\$

On Monday and Tuesday I started learning Erlang through the Erlang Koans, and around Wednesday night I decided to use my newfound knowledge to implement a simple text editor (Akin to ed(1)). Currently I have the following (also on GitLab):

-module(text).
-compile(export_all).

%% State => {file, buffer, cursor}
main() ->
  {ok, Arg} = init:get_argument(f),
  Name = lists:nth(1, lists:nth(1, Arg)),
  main(Name).

main (String) ->
  {ok, File} = file:open(String, [read, write]),
  {ok, Buffer, Lines} = buffer(File),
  io:fwrite("~B lines read of ~s~n", [Lines, String]),
  loop({File, Buffer, 1}).

loop(State) ->
  NewState = command(State),
  case NewState of
    {_, _, _} -> loop(NewState);
    _ -> ok
    end.

append(Buffer, Line) ->
  erlang:insert_element(tuple_size(Buffer)+1, Buffer, Line).

buffer(File) ->
  buffer(File, {}).
buffer(File, Buffer) ->
  Line = io:get_line(File, ""),
  case Line of
    eof -> {ok, Buffer, tuple_size(Buffer)};
    {error, Description} -> {error, Description};
    _ -> buffer(File, append(Buffer, Line))
    end.

put(stdout, {_, {}, _}, _) ->
  'nothing to write';
put(stdout, {_, Buffer, _}, Line) ->
  io:fwrite(" ~s", [element(Line, Buffer)]);
put(file, {File, Buffer, _}, Line) ->
  io:fwrite(File, "~s", [element(Line, Buffer)]).

%% Ew Ew Ew Ew Ew
write_buffer(stdout, {File, Buffer, Pos}) ->
  write_buffer(stdout, {File, Buffer, Pos}, 1, tuple_size(Buffer)).
write_buffer({File, Buffer, Pos}) ->
  file:position(File, bof),
  write_buffer(file, {File, Buffer, Pos}, 1, tuple_size(Buffer)).

write_buffer(Out, State, N, Length) when N =< Length ->
  put(Out, State, N),
  write_buffer(Out, State, N+1, Length);
write_buffer(_, {File, {}, Pos}, _, _) ->
  file:truncate(File),
  {File, {}, Pos};
write_buffer(_, State, _, _) ->
  State.

put_cursor(N, {File, Buffer, Pos}) ->
  put_cursor(Pos, Pos+N, {File, Buffer, Pos}).
put_cursor(Current, End, State) when Current < End ->
  put(stdout, State, Current),
  put_cursor(Current+1, End, State);
put_cursor(_, _, State) ->
  State.

is_stop(Line) ->
  (string:len(Line) == 2) and (string:substr(Line, 1, 1) == ".").

input() ->
  Line = io:get_line(" "),
  case is_stop(Line) of
    false -> Line;
    true -> stop
  end.

modify(replace, State) ->
  modify(replace, State, input());
modify(append, State) ->
  modify(append, State, input());
modify(change, State) ->
  modify(change, State, input()).

modify(_, State, stop) ->
  State;
modify(replace, {File, Buffer, Pos}, Input) ->
  modify(replace, {File, replace(Buffer, Pos, Input), Pos+1}, input());
modify(append, {File, Buffer, Pos}, Input) ->
  modify(append, {File, insert(Buffer, Pos+1, Input), Pos+1}, input());
modify(change, {File, Buffer, Pos}, Input)
  when tuple_size(Buffer) == 0; Pos =< 0; Pos > tuple_size(Buffer) ->
  modify(append, {File, replace(Buffer, Pos, Input), Pos}, input());
modify(change, State, Input) ->
  modify(append, State, Input).

replace(Buffer, Pos, _)
  when tuple_size(Buffer) == 0; Pos =< 0; Pos > tuple_size(Buffer) ->
  Buffer;
replace(Buffer, Pos, Input) ->
  setelement(Pos, Buffer, Input).

insert(Buffer, Pos, Input) when Pos =< 0 ->
  erlang:insert_element(1, Buffer, Input);
insert(Buffer, Pos, Input) when Pos > tuple_size(Buffer) ->
  erlang:insert_element(tuple_size(Buffer) + 1, Buffer, Input);
insert(Buffer, Pos, Input) ->
  erlang:insert_element(Pos, Buffer, Input).

guard_position({File, Buffer, Pos}) when tuple_size(Buffer) == 0; Pos < 1 ->
  {File, Buffer, 1};
guard_position({File, Buffer, Pos}) when Pos > tuple_size(Buffer) ->
  {File, Buffer, tuple_size(Buffer)};
guard_position(State) ->
  State.

get_arg(String) when is_list(String) ->
  {Integer, _} = string:to_integer(String),
  case is_integer(Integer) andalso Integer > 0 of
    true -> Integer;
    _    -> 1
  end;
get_arg(_) -> 1.

position([Char|Tail], {File, Buffer, Pos}) ->
  case Char of
    %% +, -, <number>
    43 -> {File, Buffer, Pos+1};
    45 -> {File, Buffer, Pos-1};
    _ -> {Integer, "\n"} = string:to_integer([Char] ++ Tail),
         {File, Buffer, Integer}
    end.

remove(N, {File, Buffer, Pos}) ->
  {File, erlang:delete_element(N, Buffer), Pos}.

delete([], {File, Buffer, Pos}) ->
  remove(Pos, {File, Buffer, Pos});
delete([Char|Tail], {File, Buffer, Pos}) ->
  case Char of
    %% *,
    42 -> {File, {}, Pos};
    _  -> remove(get_arg([Char] ++ Tail), {File, Buffer, Pos})
  end;
delete(N, {File, Buffer, Pos}) ->
  delete(Pos, Pos+N, {File, Buffer, Pos}).

delete(Current, End, State) when Current < End ->
  delete(Current+1, End, remove(Current, State));
delete(_, _, State) ->
  State.

search(_, Line, {File, Buffer, Pos}) when Line > tuple_size(Buffer) ->
  io:fwrite("not found~n"),
  {File, Buffer, Pos};
search(String, Line, {F, Buffer, Pos}) when tuple_size(Buffer) > 0, Line > 0 ->
  case string:str(element(Line, Buffer), String) of
    0 -> search(String, Line+1, {F, Buffer, Pos});
    _ -> {F, Buffer, Line}
  end.

search(String, {File, Buffer, Pos}) when is_list(String) ->
  search(string:strip(String, both, $\n), Pos+1, {File, Buffer, Pos});
search(_, State) ->
  io:fwrite("?~n"),
  State.

join({File, Buffer, Pos}) ->
  {File, join(Pos, Buffer), Pos}.
join(Line, Buffer) when Line+1 =< tuple_size(Buffer) ->
  Concat = string:join([string:strip(element(Line, Buffer), right, $\n),
                        element(Line+1, Buffer)], ""),
  NewBuffer = setelement(Line, Buffer, Concat),
  erlang:delete_element(Line+1, NewBuffer);
join(_, Buffer) ->
  Buffer.

help() ->
  HelpString =
"erled -- erlang line editor, Finn O'leary\
Started on 2016-05-31. Current iteration: 2016-06-03_3.\
commands:\
  q, ?, w, p    -- quit, help, write file, print file.\
  a, c, r       -- append, change, replace.\
  Please note that append, change and replace all accept
  input until a single line with a `.` has been input.\

  d, d*, d<n>   -- delete line, file, <n> lines.\
  P, P<n>       -- print line, lines from current position.\
  /<s>, j       -- search for <s>, join next line to current.\
  g+, g-, g<n>  -- next line, prev line, go to line <n>.\
",
  io:fwrite(HelpString).

command(State) ->
  command(io:get_line(""), guard_position(State)).
command([Char|_Tail], State) ->
  case Char of
    %% q, w, c, a, r, p, P, g, d, /, j, ?, ...
    113 -> {ok, State};
    119 -> write_buffer(State);
     99 -> modify(change, State);
     97 -> modify(append, State);
    114 -> modify(replace, State);
    112 -> write_buffer(stdout, State);
     80 -> put_cursor(get_arg(_Tail), State);
    103 -> position(_Tail, State);
    100 -> delete(_Tail, State);
     47 -> search(_Tail, State);
    106 -> join(State);
     63 -> help(), State;
      _ -> io:fwrite("?~n"), State
    end.

On the whole I dislike the design, as I feel that I repeat some patterns rather too much. I also feel that the code is rather verbose. Could you give any advice on how to clean it up and perhaps make it idiomatic for Erlang code? I toyed with the idea of using gen_fsm, but it seemed like overkill for my needs at the time. I also attempted to use records at some point, but found I somehow ended up with more code than I started with (In part due to the State#state.foo syntax).

\$\endgroup\$
2
\$\begingroup\$

What should the code do?

Below are some corrections that I would do:

.1. Instead of:

{ok, Arg} = init:get_argument(f),
Name = lists:nth(1, lists:nth(1, Arg)),
main(Name).

Do:

{ok, [[Name, _], _]} = init:get_argument(f),
main(Name).

Does this fragment work correctly?

.2. Instead of:

modify(replace, State) ->
  modify(replace, State, input());
modify(append, State) ->
  modify(append, State, input());
modify(change, State) ->
  modify(change, State, input()).

Do:

modify(Action, State) ->
  modify(Action, State, input()).

.3. Instead of:

delete([], {File, Buffer, Pos}) ->
  remove(Pos, {File, Buffer, Pos});
delete([Char|Tail], {File, Buffer, Pos}) ->
  case Char of
    %% *,
    42 -> {File, {}, Pos};
    _  -> remove(get_arg([Char] ++ Tail), {File, Buffer, Pos})
  end;
delete(N, {File, Buffer, Pos}) ->
  delete(Pos, Pos+N, {File, Buffer, Pos}).

Do:

delete([], {File, Buffer, Pos}) ->
  remove(Pos, {File, Buffer, Pos});
delete([42 = Char|Tail], {File, Buffer, Pos}) ->
  {File, {}, Pos};
delete([Char|Tail], {File, Buffer, Pos}) ->
  remove(get_arg([Char] ++ Tail), {File, Buffer, Pos});
delete(N, {File, Buffer, Pos}) ->
  delete(Pos, Pos+N, {File, Buffer, Pos}).

Otherwise the code looks quite OK. If it looks repetitive then it's probably because the State and common tuples are being dragged along to all the functions. Some of it could also be down to the choice of data structures used to store data, i.e. why Buffer is a tuple and not a list? In Erlang it's much easier to manipulate variable-size data with lists than with tuples.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.