For the past 10 years, my programming experience has mostly focussed on JavaScript and PHP. I've learned a few functional programming concepts through JavaScript, such as function purity, immutability, and side effects, but I've never worked with a true functional language. To learn more about functional programming, I've decided to learn Elixir and write about what I learn to help me learn.
I've been going through the Pragmatic Studio Elixir course, and this post is based on one of the exercises. I highly recommend this course!
Let's say we have the following JSON string:
json = ~s([
{
"id": 1,
"name": "Teddy",
"type": "Brown",
"hibernating": true
},
{
"id": 2,
"name": "Smokey",
"type": "Black"
},
{
"id": 3,
"name": "Paddington",
"type": "Brown"
}
])
Note, ~s
is a sigil and it allows us to easily create a string of JSON without having to escape all of the quotes.
We can parse this JSON into a list of maps using the Poison library:
bears = Poison.decode!(json)
# [
# %{"hibernating" => true, "id" => 1, "name" => "Teddy", "type" => "Brown"},
# %{"id" => 2, "name" => "Smokey", "type" => "Black"},
# %{"id" => 3, "name" => "Paddington", "type" => "Brown"}
# ]
What if we wanted this to be a list of Bear
structs instead?
The Bear
struct used in the course is similar to the following:
defmodule Bear do
defstruct id: nil, name: "", type: "", hibernating: false
end
To convert each map to a Bear
struct, we can map over the list:
Enum.map bears, fn(bear) ->
%Bear{
id: bear["id"],
name: bear["name"],
type: bear["type"],
hibernating: (if bear["hibernating"], do: true, else: false)
}
end
# [
# %Bear{hibernating: true, id: 1, name: "Teddy", type: "Brown"},
# %Bear{hibernating: false, id: 2, name: "Smokey", type: "Black"},
# %Bear{hibernating: false, id: 3, name: "Paddington", type: "Brown"}
# ]
This works, but setting each key-value pair is pretty tedious. After some googling, I found the Kernel.struct/2
function, which allows us to create a struct from a map.
So I tried this:
Enum.map bears, fn(bear) ->
struct(Bear, bear)
end
# [
# %Bear{hibernating: false, id: nil, name: "", type: ""},
# %Bear{hibernating: false, id: nil, name: "", type: ""},
# %Bear{hibernating: false, id: nil, name: "", type: ""}
# ]
Hmmm, not what I was expecting. All of the values are set to the struct's default values.
After reading about Kernel.struct/2
a bit more, it turns out that string keys are ignored. The keys of the map must be atoms. To convert the keys from strings to atoms, we can use Map.new/2
. This function allows us to create a new map from an existing map.
Enum.map bears, fn(bear) ->
bear = Map.new bear, fn({key, value}) ->
{String.to_atom(key), value}
end
struct(Bear, bear)
end
# [
# %Bear{hibernating: true, id: 1, name: "Teddy", type: "Brown"},
# %Bear{hibernating: false, id: 2, name: "Smokey", type: "Black"},
# %Bear{hibernating: false, id: 3, name: "Paddington", type: "Brown"}
# ]
Great, it works!
Now it turns out, there is an even easier way to do this using the Poison library.
Poison.decode!(json, as: [%Bear{}])
Unfortunately, I couldn't find the documentation for this in the Poison.decode!/2 v3.1.0 docs, but thankfully I learned about it in the Pragmatic Studio Elixir course!