x4lldux / disc_union

Discriminated unions for Elixir
MIT License
64 stars 1 forks source link

Parameterized typespecs - poorman's polymorphic types from OCaml #3

Closed x4lldux closed 4 years ago

x4lldux commented 8 years ago

This is to gather input and valid use cases that would benefit from supporting parameterized typespecs.

Current implementation can be used like this:

defmodule ParsedBody do .. end # just a struct
defmodule ParseError do ... end # just a struct
defmodule ValidationError do ... end # just a struct

defmodule ParseResult do
  use DiscUnion

  defunion :ok in ParsedBody.t | :error in ParseError.t
end

defmodule ValidationResult do
   use DiscUnion

  defunion :ok  in ParsedBody.t | :error in ValidationError.t
end

defmodule UsageExample do
  require ParseResult
  require ValidationResult

  def foo do
    ParseResult.c! :ok, %ParsedBody{}
    ValidationResult.c! :error, %ValidationError{}
  end
end

This will generate two different structs/unions with typespecs that will look like:

defmodule ParseResult do
  @type t() :: %ParseResult{case: {:ok, ParsedBody.t()} | {:error, ParseError.t()}}
  @spec c!(:error, ParseError.t()) :: %ParseResult{case: {:error, ParseError.t()}}
  @spec c!(:ok, ParsedBody.t()) :: %ParseResult{case: {:ok, ParsedBody.t()}}
end

defmodule ValidationResult do
  @type t() :: %ValidationResult{case: {:ok, ParsedBody.t()} | {:error, ValidationError.t()}}
  @spec c!(:error, ValidationError.t()) :: %ValidationResult{case: {:error, ValidationError.t()}}
  @spec c!(:ok, ParsedBody.t()) :: %ValidationResult{case: {:ok, ParsedBody.t()}}
end

The proposed idea is to have parametrized types and support for creating specialized version of a more general unions. Lets say we want a generic Result union and use it with specialized types for different returned data types hold in the same structure - some think akin to polymorphic types in OCaml - than creating separate modules, ParseResult and ValidationResult, and useing our general union, would populate that modules with defdelagtes (the constructor functions) to Result module but with specialized types:

defmodule ParsedBody do .. end #  ust a struct
defmodule ParseError do ... end # just a struct
defmodule ValidationError do ... end # just a struct

defmodule Result do
  use DiscUnion, type_params: [:content, :error_reason]

  defunion :ok in content | :error in error_reason
end

defmodule ParseResult do
  use Result, type_params: [content: ParsedBody.t, error_reason: ParseError.t]
end

defmoduule ValidationReslt do
  use Result, type_params: [content: ParsedBody.t, error_reason: ValidationError.t]
end

defmodule UsageExample do
  require ParseResult
  require ValidationResult

  def foo do
    ParseResult.c! :ok, %ParsedBody{}
    ValidationResult.c! :error, %ValidationError{}
  end
end

this would creat one struct/union, but with two different proxy modules with specs like this:

defmodule Result do
  @type t(content, error_reason) :: %Result{case: {:ok, content()} | {:error, error_reason()}}
  @spec c!(:error, any()) :: %Result{case: {:error, any()}}
  @spec c!(:ok, any()) :: %Result{case: {:ok, any()}}
end

defmodule ParseReslut do
  @type t() :: Result.t(ParsedBody.t, ParseError.t)
  @spec c!(:error, ParseError.t) :: %Result{case: {:error, ParseError.t()}}
  @spec c!(:ok, ParsedBody.t()) :: %Result{case: {:ok, ParsedBody.t()}}
end

defmodule ValidationResult do
  @type t() :: Result.t(ParsedBody.t, ValidationError.t)
  @spec c!(:error, ValidationError.t) :: %Result{case: {:error, ValidationError.t()}}
  @spec c!(:ok, ParsedBody.t()) :: %Result{case: {:ok, ParsedBody.t()}}
end

The usage in both cases is basically the same, the only exception being what is returned. In the second case, it would be that general %Result{} discriminated union. While in the first case it would be either a %ParseResult{} or %ValidationResult{}.

The big question is: is it worth it? Are there any real-world use cases that would require those parametrized union cases like in the second example? Dialyzer is not a static-typing analysis tool but a success-typing analysis tool so I'm not really sure if it even will be able to benefit from that way of doing things.