dogweather
EDIT: Parse, don't validate (2019) | Hacker News
I finished this working code which takes in an HTML page and outputs legal citations it finds. I like the code, and I think it’s pretty much Elixir style. E.g., it uses the shape of the data when it can. But I realized that it has an ordering dependency in the logic. In bigger projects, this can lead to bugs:
@spec find_citations(binary()) :: list()
@doc """
Find citations in a string of HTML.
"""
def find_citations(html) do
{:ok, document} = Floki.parse_document(html)
leginfo_urls =
document
|> Floki.attribute("a", "href")
|> List.flatten()
|> Enum.map(&URI.parse/1)
|> Enum.filter(&leginfo_url?/1)
leginfo_urls
|> Enum.map(&leginfo_url_to_cite/1)
|> Enum.sort()
|> Enum.uniq()
end
defp leginfo_url?(%{host: "leginfo.legislature.ca.gov"}), do: true
defp leginfo_url?(_), do: false
defp leginfo_url_to_cite(%{query: query}) do
query
|> URI.decode_query()
|> make_cite()
end
defp make_cite(%{"lawCode" => code, "sectionNum" => section}) do
"CA #{@code_abbrevs[@cal_codes[code]]} Section #{section}"
|> String.replace_suffix(".", "")
end
I.e., my leginfo_url?() predicate validates the data and just returns a boolean. And so, the code has to be written correctly so that it’s called before leginfo_url_to_cite().
I think that the “parse don’t validate” idea is meant to remedy this. Instead of returning a boolean, one would return a type that can only be obtained by a valid parse. This way, instead of the programmer remembering to check for implicit dependencies, we enable the compiler to do it for us.
Does anybody here use that approach with Elixir? I suppose that with the above code, that’d mean creating a struct with statically defined keys :law_code and :section_num. And then the function heads would be written to only accept the named struct.
For this small code—which also has complete test coverage—I’m not sure if it’s worth the work. But in larger codebases, maybe it’d make sense. ?
Trending in Discussions
Other Trending Topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #elixirconf
- #channels
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #blog-post
- #ai
- #phoenix_html
- #elixirconf-us
- #iex
- #graphql
- #genstage
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #hex
- #security
- #metaprogramming










Showing Posts 1 to 5- Show Best Posts
- Show All (oldest first)
- Show All (newest first)
ken-kost
Is this something you had in mind (not tested)? In the above code nothing happens in else clause of with, the accumulator is just passed, but you could add here different pattern matches and maybe also parse the data and add it to the accumulator.
Also, since there’s a lot of enum calls consider using stream.
Maybe I don’t understand what is parse don’t validate from haskell.
krasenyp
As @ken-kost said, you might want to use stream. Instead of
List.flatten/1you can useStream.transform/3. Your final call can beEnum.sort/1which will turn the stream into a list.dogweather
jhogberg
While types are very useful, it’s still a good idea to do this in untyped (“dynamically typed”) languages like Elixir and Erlang. Transforming external data to a known structure that your application understands and refusing to operate on anything but that eliminates lots of possible issues on its own, and is frankly less work in the long run than validating and operating on more-or-less raw data. Don’t let the absence of types stop you from implementing a good idea.
You don’t have to go so far as to declare a whole new struct every time, either, as you get many of the benefits just from consistently combining functions like
leginfo_url?andleginfo_url_to_citeinto a function that returns{:error, reason} | {:ok, {:ad_hoc_tag, value}}. For small things that can be good enough.Adzz
There are lots of data validation / casting libraries in Elixir that help with this sort of thing in different use cases. I wrote one myself GitHub - Adzz/data_schema: Declarative schemas for data transformations. · GitHub
but as mentioned it doesn’t mean everything has to be a non primitive type, it just means you should do type casting at the edge of the system and be confident that if you get further in that that type casting happened.