Open expede opened 8 years ago
The following code is giving Compilation error
def mod(num, denom) do
inter = rem(num, denom)
if inter < 0 do denom + inter else denom end
end
defdata Clock do
hour: mod(integer, 24)
minute: mod(integer, 60)
end
Here is my code
defmodule ClockModule do
def mod(num, denom) do
inter = rem(num, denom)
if inter < 0 do denom + inter else denom end
end
defdata Clock do
hour :: mod(integer, 24) # I replaced ' : ' with ' :: ' and now I get new error "type: mod/2 undefined"
minute :: mod(integer, 60)
end
end
Is there a fix for this. I also tried
defdata Clock, validate: mod do
like it was mentioned in the other issue. I get the following error with this approach " undefined function defdata/3 "
Is there a fix for this. I also tried
defdata Clock, validate: mod do
Validators and filters do not exist yet. This Issue is an idea for a potential future feature, and is not expected to work in the current version of the library.
defdata Clock do hour :: mod(integer, 24) # I replaced ' : ' with ' :: ' and now I get new error "type: mod/2 undefined" minute :: mod(integer, 60) end
The ::
operator is for declaring types, not values. Elixir has no concept of liquid types, so we're restricted to the built-in ones, or combinations of them. This means that you cannot express "integers from 0-60" at the type level, but you can enforce this when creating values (like the newtype
"smart constructor" tricks in Haskell).
If I'm reading your code correctly, I think that maybe you're trying to do something like this (with today's features):
defmodule Clock do
def mod(num, denom) do
inter = rem(num, denom)
if inter < 0, do: denom + inter, else: denom
end
defdata do
hour :: non_neg_integer()
minute :: non_neg_integer()
end
def new(hour, minute) do
%Clock{
hour: mod(hour, 24),
minute: mod(minute, 60)
}
end
end
Yep. That's it. Thanks for reply. 💯 👍
A Haskell or PureScript record would be a simple map in Elixir, right? So type Username = { first :: String, last :: String}
would be %Username{first: nil, last: nil}
in Elixir.
sidenote: Because all Algae types are structs, if there would be a Record
type, it could use @enforce_keys
for required fields.
A Haskell or PureScript record would be a simple map in Elixir, right?
Ehhhh, a haskell record would more closely map to a tuple in Elixir. A row-typed record would map to a map in Elixir.
Thanks! Yeah, I haven't really gotten to learn about row-types yet, but as you can read above, I just realized that defdata
is really for product types and not records (however stupid this sounds).
edit: Is there an official forum for the Witchcraft ecosystem? Don't want to mess up the issues with my comments.
After a couple days, here's may take on #2 and #3, the way Haskell and PureScript smart constructors are working (based on my limited knowledge) where every type is responsible for their own consistency.
Some experiments are documented in #37 using two new macros, Quark.Partial.defpartialx/2
and Algae.defdatax/1
macros (experiments
branch at latest commit - the code is a mess, and most probably has lots of parts that is inefficient or downright wrong. Also, some of the outputs are extremely verbose because I was learning about macros as I went along.)
EDIT: experiments
branch (somewhat) cleaned up
defpartialx
does currying by creating named functions that can be overridden, anddefdatax
uses type checking based on bootstrapped primitive types (Algae.Prim
after PureScript's Prim
). The examples are verbose because defdatax
is only implemented for full module types yet.def mod(num, denom) do inter = rem(num, denom) if inter < 0 do denom + inter else denom end end defdata Clock do hour: mod(integer, 24) minute: mod(integer, 60) end five_minutes = %Clock{hour: -1, minute: 55} # => %Clock{hour: 23, minute: 55} %five_minutes{minute: 102} # => Clock[hour: 23, minute: 42]
Using defdatax
:
defmodule Clock do
import Algae
defdatax do
hour :: Clock.Hour
minute :: Clock.Minute
end
def new(minutes) do
mins = rem(minutes, 60)
hours = div(minutes, 60)
new(hours, minutes)
end
def new(hours, minutes) do
h = Clock.Hour.new(hours)
m = Clock.Minute.new(minutes)
super(h,m)
end
def mod(num, denom) do
inter = rem(num, denom)
if inter < 0 do denom + inter else inter end
end
defmodule Hour do
defdatax do
hour :: integer
end
def new(hour) do
hour
|> Clock.mod(24)
|> super()
end
end
defmodule Minute do
defdatax do
minute :: integer
end
def new(minute) do
minute
|> Clock.mod(60)
|> super()
end
end
end
Test:
iex(38)> Clock.new(-1, 55)
%Clock{hour: %Clock.Hour{hour: 23}, minute: %Clock.Minute{minute: 55}}
iex(39)> Clock.new(202)
%Clock{hour: %Clock.Hour{hour: 3}, minute: %Clock.Minute{minute: 22}}
defdata AcuteTriangle, validate: acute do angle1: float angle2: float end defp acute(%AcuteTriangle{angle1: angle1, angle2: angle2}) do: abs(angle1 - angle2) < 90.0
Using defdatax
:
defmodule AcuteTriangle do
import Algae
defdatax do
angle1 :: float
angle2 :: float
end
def new(alfa, beta) do
# well, overriding a constructor also overrides type checking...
super(alfa, beta)
case abs(alfa - beta) < 90.0 do
false ->
raise(ArgumentError, "angles are not acute")
true ->
super(alfa, beta)
end
end
end
Testing:
iex(47)> AcuteTriangle.new(2.0, 127.0)
** (ArgumentError) angles are not acute
iex:54: AcuteTriangle.new/2
iex(47)>
iex(42)> AcuteTriangle.new
#Function<1.38387210/1 in AcuteTriangle.new/0>
iex(43)> AcuteTriangle.new.(2)
** (ArgumentError) not float
(algae) lib/algae/prim.ex:12: Algae.Prim.float/1
iex:43: anonymous fn/2 in AcuteTriangle.new/1
(elixir) lib/enum.ex:1925: Enum."-reduce/3-lists^foldl/2-0-"/3
iex:43: AcuteTriangle.new/1
iex(43)> AcuteTriangle.new(2.0)
#Function<3.38387210/1 in AcuteTriangle.new/1>
iex(44)> AcuteTriangle.new(2.0).(27.0)
%AcuteTriangle{angle1: 2.0, angle2: 27.0}
iex(45)> AcuteTriangle.new(2.0).(27)
** (ArgumentError) not float
(algae) lib/algae/prim.ex:12: Algae.Prim.float/1
iex:43: anonymous fn/2 in AcuteTriangle.newp/2
(elixir) lib/enum.ex:1925: Enum."-reduce/3-lists^foldl/2-0-"/3
iex:43: AcuteTriangle.newp/2
iex(45)> AcuteTriangle.new.(2.0).(27.0)
%AcuteTriangle{angle1: 2.0, angle2: 27.0}
iex(46)> AcuteTriangle.new(2.0, 27)
** (ArgumentError) not float
(algae) lib/algae/prim.ex:12: Algae.Prim.float/1
iex:43: anonymous fn/2 in AcuteTriangle.newp/2
(elixir) lib/enum.ex:1925: Enum."-reduce/3-lists^foldl/2-0-"/3
iex:43: AcuteTriangle.newp/2
iex:50: AcuteTriangle.new/2
iex(46)> AcuteTriangle.new(2.0, 27.0)
%AcuteTriangle{angle1: 2.0, angle2: 27.0}
defmodule Person do
import Algae
defdatax do
name :: string
age :: integer
end
end
defmodule Employee do
import Algae
defdatax do
person :: Person
role :: string
end
def new(person), do: raise(UndefinedFunctionError, "locked")
end
Testing Person
:
iex(7)> Person.new
#Function<1.56597431/1 in Person.new/0>
iex(8)> Person.new("lofa")
#Function<3.56597431/1 in Person.new/1>
iex(9)> Person.new.("lofa")
#Function<3.56597431/1 in Person.new/1>
iex(10)> Person.new(27)
** (ArgumentError) not string
(algae) lib/algae/prim.ex:6: Algae.Prim.string/1
iex:7: anonymous fn/2 in Person.new/1
(elixir) lib/enum.ex:1925: Enum."-reduce/3-lists^foldl/2-0-"/3
iex:7: Person.new/1
iex(10)> Person.new.(27)
** (ArgumentError) not string
(algae) lib/algae/prim.ex:6: Algae.Prim.string/1
iex:7: anonymous fn/2 in Person.new/1
(elixir) lib/enum.ex:1925: Enum."-reduce/3-lists^foldl/2-0-"/3
iex:7: Person.new/1
iex(10)> Person.new("lofa").(27)
%Person{age: 27, name: "lofa"}
iex(11)> Person.new("lofa",27)
%Person{age: 27, name: "lofa"}
iex(12)> Person.new("lofa").(:a)
** (ArgumentError) not integer
(algae) lib/algae/prim.ex:3: Algae.Prim.integer/1
iex:7: anonymous fn/2 in Person.newp/2
(elixir) lib/enum.ex:1925: Enum."-reduce/3-lists^foldl/2-0-"/3
iex:7: Person.newp/2
iex(12)> Person.new("lofa", :a)
** (ArgumentError) not integer
(algae) lib/algae/prim.ex:3: Algae.Prim.integer/1
iex:7: anonymous fn/2 in Person.newp/2
(elixir) lib/enum.ex:1925: Enum."-reduce/3-lists^foldl/2-0-"/3
iex:7: Person.newp/2
Testing Employee
:
iex(13)> Employee.new
#Function<1.49791001/1 in Employee.new/0>
iex(14)> Employee.new.(27)
** (UndefinedFunctionError) undefined function
iex:14: Employee.new/1
iex(14)> Employee.new(27)
** (UndefinedFunctionError) undefined function
iex:14: Employee.new/1
iex(14)> Employee.new(27, "janitor")
** (FunctionClauseError) no function clause matching in Person.type/1
The following arguments were given to Person.type/1:
# 1
27
iex:7: Person.type/1
iex:9: anonymous fn/2 in Employee.newp/2
(elixir) lib/enum.ex:1925: Enum."-reduce/3-lists^foldl/2-0-"/3
iex:9: Employee.newp/2
iex(14)> Employee.new(%Person{name: "lofa", age: 27}, "janitor")
%Employee{person: %Person{age: 27, name: "lofa"}, role: "janitor"}
iex(15)> Employee.new(Person.new.("lofa").(27), "janitor")
%Employee{person: %Person{age: 27, name: "lofa"}, role: "janitor"}
iex(16)> Employee.new(Person.new.(2).(27), "janitor")
** (ArgumentError) not string
(algae) lib/algae/prim.ex:6: Algae.Prim.string/1
iex:7: anonymous fn/2 in Person.new/1
(elixir) lib/enum.ex:1925: Enum."-reduce/3-lists^foldl/2-0-"/3
iex:7: Person.new/1
iex(16)>
An example:
defmodule BinaryId do
import Algae
defdatax do
binary_id :: binary
end
def new() do
# Ecto.UUID.generate()
# |> new()
new("binary_id")
end
def type(%__MODULE__{binary_id: "binary_id"}) do
# Ecto.UUID.cast!(binary_id)
end
def type(_), do: raise(ArgumentError, "not #{__MODULE__}")
end
defmodule User do
import Algae
defdatax do
user_id :: BinaryId
name :: string
end
end
iex(4)> BinaryId.new
%BinaryId{binary_id: "binary_id"}
iex(5)> BinaryId.new("lofa")
** (ArgumentError) not Elixir.BinaryId
iex:18: BinaryId.type/1
iex:4: BinaryId.newp/1
iex(12)> User.new(BinaryId.new())
#Function<3.96843422/1 in User.new/1>
iex(13)> User.new(BinaryId.new()).("lofa")
%User{
name: "lofa",
user_id: %BinaryId{binary_id: "87063522-8bd5-42fe-876a-22f3afa12b6c"}
}
iex(16)> User.new("binary")
** (FunctionClauseError) no function clause matching in BinaryId.type/1
The following arguments were given to BinaryId.type/1:
# 1
"binary"
iex:21: BinaryId.type/1
iex:14: anonymous fn/2 in User.new/1
(elixir) lib/enum.ex:1925: Enum."-reduce/3-lists^foldl/2-0-"/3
iex:14: User.new/1
iex(16)> User.new(<<1,2,3>>)
** (FunctionClauseError) no function clause matching in BinaryId.type/1
The following arguments were given to BinaryId.type/1:
# 1
<<1, 2, 3>>
iex:21: BinaryId.type/1
iex:14: anonymous fn/2 in User.new/1
(elixir) lib/enum.ex:1925: Enum."-reduce/3-lists^foldl/2-0-"/3
iex:14: User.new/1
A Haskell or PureScript record would be a simple map in Elixir, right?
A Haskell record is here modeled with an Elixir struct
the way Haskell and PureScript smart constructors
Haskell's smart constructors are just regular functions without the handy sugar, and generally hiding the normal constructor.
Yes, this is the general idea from the issue. I'd be happy to have this functionality if your branch is all good 👍
Hmm, it would be good if the smart constructor didn't need to do this (also if we can avoid subtyping, that would be ideal.) Algae does need some TLC to add type variables and whatnot (the autogenerated types right now are pretty "dumb"), which may help with this issue, if I'm understanding correctly.
You can hide the main struct syntax constructor with a use
, but people can always import
and get access to the %Foo{}
syntax. Structs aren't as guarded in Elixir, and it's absolutely possible to arbitrarily add or alter fields in an Elixir struct. The most common ways of using a struct will check fields, but they're not guaranteed across all functions.
Special Syntax
Yes, this is the general idea from the issue. I'd be happy to have this functionality if your branch is all good
I'll clean things up, and will do a pull request for you to review then. Thanks!
Overriding Type Checking
You're right, I was totally overthinking this.
Hiding the Data Constructor
You can hide the main struct syntax constructor with a
use
I think I'm missing a very basic thing here, because this is new. Would you give an example?
Structs aren't as guarded in Elixir, and it's absolutely possible to arbitrarily add or alter fields in an Elixir struct.
Yes, just realized a couple days ago that even though defdata
and defsum
are the Algae way to create product types and sum types, but in the end the result is an Elixir struct.
A Haskell record is here modeled with an Elixir struct
Thanks again. As I realized above, I have to stop perceiving Algae as Haskell in Elixir. Algae and others in this family allow more control, but it's still plain Elixir. (I feel stupid reading back the last sentence, but it took me some time to get there...)
Similar to what can be achieved with a Haskell
newtype
, provide a function to modify or validate an incoming value.Example: