I wrote some script in line with an exercise on Macros.
The script is meant to parse arithmetic and output a natural language version.
Is there a way I could be more concise, clear, or in-line with best practices in my implementation?
omap = %{
+: "plus",
-: "subtract",
*: "multiplied by",
**: "to the power of",
}
quote do
9**3*3*3*5**5 + 8*8 + 7*9**2**9**2*5
end ###Transcriber below###
|> Macro.postwalk(
fn
{op, _meta, [x, y]} ->
all_processed = Enum.all?([x, y], &is_binary/1) #When an fragment describing an operation has been processed it is converted into a string
case all_processed do #leverages that right to left ordering gives indication of operation precedent
false when is_binary(x) ->
x <> " then #{omap[op]} #{y}"
true ->
x <> " then #{omap[op]} " <> y
_ ->
"#{x} #{omap[op]} #{y}"
end
node -> node
end)
#==> "9 to the power of 3 then multiplied by 3 then multiplied by 3 then multiplied by
#5 to the power of 5 then plus 8 multiplied by 8 then plus 7 multiplied by 9 to the power
#of 2 then to the power of 9 then to the power of 2 then multiplied by 5
Note: This doesn’t really work well with brackets. I think to get brackets to work as expected I’d need to do a prewalk and add a kwl to the relevant nodes (nodes with non compliant precedenting) to indicate parenthesis and depth of nesting, but I’m not really looking to go that far with this exercise.
I think your original logic would benefit from either function guards or cond. E.g.:
Macro.postwalk(ast, fn
{op, _meta, [x, y]} ->
cond do
is_binary(x) and is_binary(y) -> x <> " then #{omap[op]} " <> y
is_binary(x) -> x <> " then #{omap[op]} #{y}"
true -> "#{x} #{omap[op]} #{y}"
end
node ->
node
end)
But your first and second clauses are actually the same, so I think you can simplify to this with guards:
Macro.postwalk(ast, fn
{op, _meta, [x, y]} when is_binary(x) -> x <> " then #{omap[op]} #{y}"
{op, _meta, [x, y]} -> "#{x} #{omap[op]} #{y}"
node -> node
end)
defmodule Example do
@omap %{
+: " plus ",
-: " subtract ",
*: " multiplied by ",
**: " to the power of "
}
def sample(input) do
Macro.postwalk(input, fn
{op, _meta, [x, y]} when is_integer(x) ->
[Integer.to_string(x), @omap[op], integer_to_string(y)]
{op, _meta, [x, y]} ->
[x, " then", @omap[op], integer_to_string(y)]
node ->
node
end)
end
defp integer_to_string(integer) when is_integer(integer), do: Integer.to_string(integer)
defp integer_to_string(iolist) when is_list(iolist), do: iolist
end
iodata = Example.sample(quote do
9 ** 3 * 3 * 3 * 5 ** 5 + 8 * 8 + 7 * 9 ** 2 ** 9 ** 2 * 5
end)
IO.puts(iodata)
# or
# List.to_string(iodata)
String concatenation in each Macro.postwalk/2 call is slow. It’s better to use iodata instead as we simply create a nested list of strings to be printed.
See IO data section in IO module documentation for more information.
Ah! This is really concise thanks for seeing this.
Thanks for both your answers, I can combine the two to make it faster and more conciselol already achieved by Eiji. Appreciate it.
Do you happen to know of any resources for tending towards these design patterns? BGg I’ll soon start Elixir in Action, and I’ve been hearing there is emphasis on design patterns in the book.
Again, the documentation should be enough. Below linked section the next one explains use cases i.e. why and when to prefer iodata over strings. It’s not really about design, but about performance.