Here is a modification of @kokolegorille’s example, from an editor with some more structure:
defmodule Sandbox.Post do
# `label` is the only key that needs explicit setting
@keys [:label, has_footnote: false, children: []]
@enforce_keys [:label]
defstruct @keys
@type t :: %__MODULE__{label: String.t(), has_footnote: boolean(), children: [t()]}
@spec set_footnote(post :: t()) :: t()
def set_footnote(%__MODULE__{label: title, children: children} = post) do
if String.ends_with?(title, "*") do
%{
post
| label: String.trim_trailing(title, "*"),
has_footnote: true,
children: Enum.map(children, &set_footnote/1)
}
else
%{post | children: Enum.map(children, &set_footnote/1)}
end
end
@spec new(label :: String.t(), children :: [t()]) :: t()
def new(label, children \\ []) do
if String.ends_with?(label, "*") do
label_without_asterisk = String.trim_trailing(label, "*")
%__MODULE__{
label: label_without_asterisk,
has_footnote: true,
children: Enum.map(children, &set_footnote/1)
}
else
%__MODULE__{label: label, children: children}
end
end
end
defmodule Sandbox.PostsRecursive do
alias Sandbox.Post
@posts [
%Post{
label: "Post with footnote*",
children: [%Post{label: "Nested post with footnote*"}, %Post{label: "No footnote here"}]
},
%Post{label: "Top-level note without footnote", children: [%Post{label: "Footnote here*"}]}
]
def example(), do: Enum.map(@posts, &Post.set_footnote/1)
end
I suggest studying @kip’s function in order to understand what is happening, as you’ll likely not have any issues with recursion as a concept if you fully understand it.
Edit: I’ve added an alternative approach using a new
function that could move the application of this logic to the creation of posts instead, provided you create them by calling that function.
This, coupled with some discipline, is useful when you want to guarantee things about datatypes and is the preferred way in languages where the norm is to not expose raw data constructors and instead have limited interfaces generally only exposing constructor/modification/accessor functions.