Struct. I want a class to hold variables, that the gist?
conceptually, very close. but having a "class" implies that we have objects and methods, and none of those exist in Elixir/Erlang.
instead, there's just data (e.g. strings, numbers, lists, maps, structs) and behavior (functions), and structs are data.
structs are also just "maps" (read: hashes) under the hood, but they have a :__struct__ key that points to the module they belong to.
i think the big question is: what do we use structs for?
my answer is: ...kinda everything.
for example, if we do a people = Repo.all(Person) call to the DB in Ecto, the result will be a list (array) of structs (glorified hashes). in Rails/ActiveRecord, the DB row is parsed into an instance of the Person class. in Phoenix/Ecto, the row data is parsed into a Person struct. this resulting struct has no behavior (i.e. methods) though, it simply holds the data from the row. to do anything with it, we have to pass it to a function. (also, when i say "function", think of a lambda or a proc; it's something that hangs alone in space with no object context.)
another example in Phoenix would be the Plug.Conn struct that holds all the information for a request, as well as our response. since the struct is just data though, and since data cannot be mutated in Elixir, we basically pass this Plug.Conn struct down through a series of functions which each create a new Plug.Conn and pass it along to the next function. (these functions include all of the framework logic, and eventually our router and controller action at the very end.) in Phoenix, this is how we make decisions on what to do, and finally render a response.
so that's some of the "what" and "why", but how do we use them?
# we can create a plain old map with the `%{}` literal syntax
map = %{
foo: "bar", # shorthand for atom keys
:baz => "boo", # but we can do this too
"str" => 123, # non-atom keys must use hashrocket (`=>`)
456 => %{what: ["ever"]}
# ^ can mix types, and nest away!
}
map.foo # => "bar" # access map via dot notation
map["foo"] # => "bar" # access map via sub notation
# there's also a `Map` module that has a
# ton of functions for working with maps...
Map.get(map, :foo) # => "bar"
# ^ use `Map.get/2` to access a value
Map.get(map, :dne, "miss") # => "miss"
# ^ `Map.get/3` to provide a default for a miss
Map.put(%{}, :key, "val") # => %{key: "val"}
# ^ and `Map.put/3` to set a key & value
# now let's define a struct...
defmodule Person do
defstruct [:first_name, :last_name]
# ^ define a struct for the module we're in (Person)
# and allow it to have `:first_name` and `:last_name`
#
# this basically means that we want a special map (hash)
# to represent `Person`, that has the following structure:
#
# %{first_name: ???, last_name: ???}
def name(person) do
"#{person.first_name} #{person.last_name}"
end
# ... and we define a function that we'll look at in a bit
end
# we can now make a `Person` struct using a struct literal
brad = %Person{first_name: "Brad", last_name: "Chase"}
brad.first_name # => "Brad"
# ^ dot notation works just like with a map
# ... because the struct *is* a map
# we can also update a struct like this...
%{ brad | first_name: "Bradley" }
# => %Person{first_name: "Bradley", last_name: "Chase"}
# but we **cannot add a key that isn't defined for the struct**
# thus, structs provide some simple validation via key filtering
%{ brad | foo: "boof" }
# ** (KeyError) key :foo not found in: %Person{...}
# it knows to perform this and other checks because...
brad.__struct__ # => Person
# since structs are maps, we can also use `Map` functions...
Map.get brad, :first_name
# => "Brad"
# but we have to be careful, because they'll ignore our key restrictions...
Map.put(brad, :foo, "woops")
# => %Person{foo: "woops", first_name: "Bradley", last_name: "Chase"}
# in addition to the struct literal notation, we can also
# pass the module and a map of attrs to `struct/2` (`Kernel.struct/2`)
#
# and notice how `struct/2` ignores the `:foo` key without a `KeyError`
ryan = struct Person, %{first_name: "Ryan", last_name: "Devlin", foo: "bar"}
# => %Person{first_name: "Ryan", last_name: "Devlin"}
# we also defined a `Person.name/1` function above.
# let's pass a struct into it to get a full name string...
Person.name(brad) # => "Brad Chase"
# the `Person` module is an obvious place to put this functionality,
# but we could arbitrarily put it somewhere else just as easily...
defmodule PersonNamerizer do
def willy_nilly_call(anything) do
"#{anything.first_name} #{anything.last_name}"
end
# ^ this function will match for *any* argument
def anything_with_first_and_last(%{first_name: f, last_name: l}) do
"#{f} #{l}"
end
# ^ this function will match for any map
# with keys `:first_name` and `:last_name`
def person_struct_only_call(%Person{} = person) do
"#{person.first_name} #{person.last_name}"
end
# ^ this function will only match for a `Person` struct argument
end
PersonNamerizer.willy_nilly_call(brad) # => "Brad Chase"
PersonNamerizer.anything_with_first_and_last(brad) # => "Brad Chase"
PersonNamerizer.person_struct_only_call(brad) # => "Brad Chase"
# we can also make a regular map out of a struct, if we so wanted
Map.from_struct %Person{first_name: "Brad", last_name: "Chase"}
# => %{first_name: "Brad", last_name: "Chase"}Elixir has the pecularity of having another way of handling key/value pairs, in the form of "keyword lists".
# a "list" is just an "array" (in Ruby or JS parlance)
list = [1,2,3]
# and in the "keyword list" context, a "keyword" is basically just a tuple...
{:some_atom_key, "any value you like"}
# which when put in a list, yields us this...
[ {:foo, "bar"} ]
# => [foo: "bar"]
# the literal syntax also works...
[foo: "bar"] # => [foo: "bar"]
# we can access values via sub notation, but *not* dot notation
[some_atom_key: "any value you like"][:some_atom_key]
# => "any value you like"
# and just as maps have the `Map` module, keyword lists have `Keyword`
Keyword.get([foo: "bar"], :foo) # => "foo"
Keyword.put([foo: "bar"], :baz, "boo") # => [baz: "boo", foo: "bar"]
# and if we don't know what we have,
# but we *do* know that it has atom keys,
# we can use the `Access` module
Access.get(%{foo: "bar"}, :foo)
# => "bar"
Access.get([foo: "bar"], :foo)
# => "bar"there is only data and behavior... and processes... but some other things to note:
- functions (anonymous and otherwise) can be passed as arguments
- there are also modules, which mostly act as namespaces for functions
- in most any useful application, there's the need for persistent state, which we can also do in Elixir, we just always do it explicitly. most often this is done using something built off of
Processes and message passing (see also:Task,Agent,GenServer).