Getting started with Phoenix with Earmark, and ElixirLS

Hello there!

So I’ve been wanting to jump in Phoenix and I thought I’d try making it more difficult for myself with introducing Earmark into the equation.

I would say setting up like a blog, however I would find the CRUD way of doing things to be somewhat silly for me because…well it’s only me creating posts with Markdown.

I do have a few questions:

  1. Should I bother with Ecto?

    I’m more wondering what the best way to store everything. I think I should store both the Markdown itself and the generated HTML, in separate tables with a shared key like ID or something. Though I’m not the most experience with databases.

  2. Potentially extending Earmark?

    The two things I’d like to do is:

    1. Have frontmatter in Markdown for posts

    2. Components of sorts

      Mainly just for use with things like code blocks and such.

    Though maybe, for now, it would just be worth…somehow using remark? Though that sounds like it be a bit more of a headache.

  3. ElixirLS to ignore certain folders.

    I like that ElixirLS exists, however I wish I could tell it to not look in every directory in my project, slowing itself down and Emacs. Is it possible to have ElixirLS ignore some folders, or is that yet to be added in it’s functionality? Potentially specifying a .gitignore?

Thank you for any answers you give!

Hello and welcome,

This post might give You some idea…

2 Likes

Oh I like that so far!

Thank you!

I feel like

defp parse_attr(:title, value),
  do: String.trim(value)

defp parse_attr(:author, value),
  do: String.trim(value)

defp parse_attr(:description, value),
  do: String.trim(value)

defp parse_attr(:body, value),
  do: value

defp parse_attr(:tags, value),
  do: value |> String.split(",") |> Enum.map(&String.trim/1) |> Enum.sort()

Could maybe be abstracted better in some way, though. As opposed to this…mutlimethod(?) of sorts. Maybe.

Ouch, this is typical Elixir code… and it’s hard to be more descriptive than this.

Maybe later You’ll enjoy this kind of code, when You’ll start to like pattern matching in function header :slight_smile:

1 Like

Is there not some kind of metaprogramming facility that allows to us to abstract this away? Or simply pattern matching with case or with?

Like…oh

case a do  # we'd have to test if `a' is of type atom, maybe with a guard
  :tags -> value |> String.split(",") |> Enum.map(&String.trim/1) |> Enum.sort()
  _ -> String.trim(value)
  :error -> "Oops!"
end

Or something?

Using abstraction and multi methods suggests You are still thinking in OOP, but in FP You don’t have methods, just Module, Function and Arguments…

Of course You can simplify this, but using metaprogramming add complexity, or use a case.

defp parse_attr(type, value) do
  case type do
    type when type in ~w(title author description)a ->
      String.trim(value)
    etc
  end
end

But most will try to avoid this case, and use multiple header function.

In the end, it’s only one function :slight_smile:

Oh, I kind of get it. Because there’s functions like fun/1 and fun/2. The functions defined are based on their name and arity, right?

Yes, if You mean Elixir will treat them as 2 distincts functions. But what You described is just one.

Compare with this code…

defp parse_attr(:title, value), do: String.trim(value)
defp parse_attr(:author, value), do: String.trim(value)
defp parse_attr(:description, value), do: String.trim(value)

It’s repetitive, but descriptive too.

You could be a little more terse with:

defp parse_attr(:body, value),
  do: value

defp parse_attr(:tags, value),
  do: value |> String.split(",") |> Enum.map(&String.trim/1) |> Enum.sort()

defp parse_attr(_, value),
  do: String.trim(value)

That does the same thing as your first example except that it will not throw an error if something other than :title, :author, :description, :body or :tags is passed as the first argument to that function. In that case it just returns String.trim(value). You could make it throw an error by using a guard (see the example below), and not defining a function clause that matches the first argument on anything.

The code below is what I think you intended to do with your second example, but there are two differences between this and that example:

  1. It returns value if the first argument is :body (the second example would return String.trim(value))
  2. It returns “Oops!” if the first argument wasn’t one of the expected ones (the second example returns String.trim(value) if the first argument is anything other than :tags).

The third clause in that case block will never match because :error would match on the second clause.

defp parse_attr(:body, value),
  do: value

defp parse_attr(:tags, value),
  do: value |> String.split(",") |> Enum.map(&String.trim/1) |> Enum.sort()

defp parse_attr(trimmed, value) when trimmed in [:title, :author, :description] do
  String.trim(value)
end

# If you leave this out, calls to this function will raise an error if the first 
# value is not one of the expected ones above.
defp parse_attr(_, value),
 do: "Oops!"

As the other poster said, this is pretty idiomatic Elixir code. I felt like it would be confusing when I started learning Elixir, but now I find it much easier to read and reason about than one or more control flow blocks.

3 Likes

You can do it with macros, but first rule is… don’t use macros.

It’s just harder to read.

iex(1)> defmodule Koko do                                                
...(1)> for type <- ~w(title author description)a do                     
...(1)>     defp parse_attr(unquote(type), value), do: String.trim(value)
...(1)> end
...(1)> end
{:module, Koko,
 <<70, 79, 82, 49, 0, 0, 4, 92, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 130,
   0, 0, 0, 13, 11, 69, 108, 105, 120, 105, 114, 46, 75, 111, 107, 111, 8, 95,
   95, 105, 110, 102, 111, 95, 95, 10, 97, ...>>,
 [parse_attr: 2, parse_attr: 2, parse_attr: 2]}

… and easy to change!

2 Likes

Another way to clean up this code without changing any semantics of how it works would be to use guard clauses:

defp parse_attr(attr, value) when attr in [:title, :author, :description], 
  do: String.trim(value)

defp parse_attr(:body, value),
  do: value

defp parse_attr(:tags, value),
  do: value |> String.split(",") |> Enum.map(&String.trim/1) |> Enum.sort()

This keeps the functionality intact where this function will reject if not called with one of the specific atoms defined in the function head, but it’s slightly less code to write for each case.

Edit: did not originally see the second part of @srowley’s example, they suggested this approach as well. Anyway, this is how I’d do it.

3 Likes

Yep, I do it like yourself and @srowley as well. I want the code to blow up if it receives an unexpected input.