Reputation: 592
I'm playing with Erlang and trying to write an S-expression parser. I find it to be an easy task in Python using stacks and loops, but it's non-trivial for me as a beginner in immutable variables and Erlang data structures.
I need to transform a list in Erlang like this:
X = ["0", "(", "1", "2", "3", ")"],
Res = transform(X). % ["0", ["1", "2", "3"]]
By now, I've come to this:
transform(List) ->
lists:map(fun(X)->
case string:equal("(", X) of
%% recursive call with sublist of List from "(" to ")" as argument
true -> transform_to_list(Lack)
end
end, List).
Not sure how to get the sublist Lack
and pass it as an argument.
Am I going in the right direction?
Upvotes: 3
Views: 614
Reputation: 14042
I know you already got an answer, but I read your question yesterday before going to the beach and I imagined this one while I was looking at the kite surf "ballet", so I give it, it is a little bit different than Steve one, so it may be interesting.
The lists:map function cannot be used in the case of this analysis, because it only apply a given function to each elements of a list to build a new list which will have the same length. No way to build nested list. As @Steve says, you need an accumulator to progressively build the results.
The lists library offers a function to accumulate terms while traversing a list: lists:foldl/3 (it exists also foldr, mapfoldl and mapfoldr), the problem in this case is to define the accumulator that will help us to build the expected result.
The simplest list to analyze have no parenthesis, so the accumulator should contain a list where to accumulate all the elements of the entry list.
But if we encounter a "(", we should start a new list that will contain the sublist we have to nest in the result. In this case we need a term that contain a list were we can put the new sublist to build, and the list that was in progress when we encounter the "(".
the simplest structure that can fit the 2 needs in a single form is a list of list: [SublistInProgress|PreviousWork]
Now we know the form of our accumulator, we can define the function in charge to build it, 3 cases:
in the shell :
1> F = fun("(",Acc)-> [[],Acc];
1> (")",[SubList,[Hacc|Tacc]]) -> [[lists:reverse(SubList)|Hacc]|Tacc];
1> (X,[Hacc|Tacc]) -> [[X|Hacc]|Tacc] end.
#Fun<erl_eval.12.52032458>
Note: I accumulate the element in the list using the construct [X|Hacc]
rather than Hacc ++ [X]
, it is a good habit because it avoids to make a completely new list at each step (and doing this I will avoid a remark from my friend @Hynek-Pichi-Vychodil :o). So I have to reverse the list when I want to store it.
Using F in the function lists:foldl(F,[[]],L)
we will get a list of one element, this element is the reverse of the expected result. So we have to embed this call to the library in a specific function:
2> Transform = fun(L) -> [R] = lists:foldl(F,[[]],L),
2> lists:reverse(R) end.
#Fun<erl_eval.6.52032458>
and we can test it:
3> L1 = ["0", "(", "1", "2", "3", ")"].
["0","(","1","2","3",")"]
4> L2 = ["0", "(", "11", "22", "(", "333", "444","(", "5555", ")", "666", ")", "77", "88", ")", "9"].
["0","(","11","22","(","333","444","(","5555",")","666",")",
"77","88",")","9"]
5> Transform(L1).
["0",["1","2","3"]]
6> Transform(L2).
["0",["11","22",["333","444",["5555"],"666"],"77","88"],"9"]
Upvotes: 3
Reputation: 20024
You can solve this using an accumulator and pattern matching:
-module(t).
-export([transform/1]).
transform(List) ->
transform(List, []).
transform([], Acc) ->
lists:reverse(Acc);
transform(["("|T], Acc) ->
transform(T, {[],Acc});
transform([")"|T], {L,{L2,Acc}}) ->
transform(T, {[lists:reverse(L)|L2],Acc});
transform([")"|T], {L,Acc}) ->
transform(T, [lists:reverse(L)|Acc]);
transform([H|T], {L,Acc}) ->
transform(T, {[H|L],Acc});
transform([H|T], Acc) ->
transform(T, [H|Acc]).
The transform/1
function just sets up an empty accumulator for transform/2
, where all the work is done.
The transform/2
function is separated into multiple pattern-matching recursive clauses:
The first clause handles the case where we've exhausted the input list, and it simply returns the reversed accumulator. Reversal is needed because items are pushed into the accumulator, so it ends up in reverse order. This is a common pattern in Erlang and other functional languages.
The second clause recognizes a "("
, which starts a new sublist. To handle it, it changes the accumulator to a 2-tuple, where the first item is a sublist accumulator and the second item is the old accumulator.
The third and fourth clauses handle ")"
, which ends a sublist. The third clause is for the case where the accumulator is a tuple holding a second element which is also a tuple; it adds the new sublist as an item to the previous sublist and pops one level from the accumulator tuple. The fourth clause handles the case where the original accumulator in the tuple is a list, adding the new sublist to the head of the original accumulator to form a new accumulator list.
The fifth and sixth clauses handle input items that are not grouping operators. The fifth clause handles the case when the accumulator is a tuple, while the sixth clause handles the case when the accumulator is a list.
Running this on your original example shows the correct answer:
1> c(t).
{ok,t}
2> t:transform(["0", "(", "1", "2", "3", ")"]).
["0",["1","2","3"]]
But it can also handle nested groups:
3> t:transform(["0", "(", "11", "22", "(", "333", "444",
"(", "5555", ")", "666", ")", "77", "88", ")", "9"]).
["0",["11","22",["333","444",["5555"],"666"],"77","88"],"9"]
Upvotes: 4