Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create/update filters #2

Open
expede opened this issue Jan 4, 2016 · 10 comments
Open

Create/update filters #2

expede opened this issue Jan 4, 2016 · 10 comments

Comments

@expede
Copy link
Member

expede commented Jan 4, 2016

Similar to what can be achieved with a Haskell newtype, provide a function to modify or validate an incoming value.

Example:

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]
@expede expede mentioned this issue Jan 4, 2016
@hariroshan
Copy link

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 "

@expede
Copy link
Member Author

expede commented Nov 23, 2018

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

@hariroshan
Copy link

Yep. That's it. Thanks for reply. 💯 👍

@toraritte
Copy link
Contributor

toraritte commented Feb 27, 2019

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.

@OvermindDL1
Copy link

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.

@toraritte
Copy link
Contributor

toraritte commented Feb 28, 2019

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.

@toraritte
Copy link
Contributor

toraritte commented Mar 5, 2019

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, and
  • defdatax 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.

#2 example

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}}

#3 example

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}

Type checking and constructor override examples

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)> 

Overriding type checking for complex types

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

@expede
Copy link
Member Author

expede commented Mar 7, 2019

A Haskell or PureScript record would be a simple map in Elixir, right?

A Haskell record is here modeled with an Elixir struct

@expede
Copy link
Member Author

expede commented Mar 7, 2019

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.

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 👍

Overriding Type Checking

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.

Hiding the Data Constructor

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.

@toraritte
Copy link
Contributor

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...)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants