与 Elixir 中的理解兼容的选项类型?

Option type compatible with comprehensions in Elixr?

背景

我正在努力提高我的函数式编程 (FP) 技能,新手在 FP 中首先要学习的一件事是 Option 类型(又名 Maybe Monad)。

选项是什么?

这种结构存在于许多语言中,Haskell 有 Maybe 和 Java 和 Python(是的,Python!)有 Optional。

基本上这种类型 模拟一个可能存在也可能不存在的值

这一切如何归结为 Elixir

大多数 FP 语言都有 comprehensions,Scala 和 Elixir 有 for 结构,而 Haskell 有其著名的 do 符号。

在 Scala 和 Haskell 中,这些理解不仅适用于可枚举类型(例如列表),而且适用于我们的 Option 类型(不是可枚举类型)。

我提到这一点,因为根据我的理解,Elixir 的理解 适用于 Enumerables。此外,据我所知,Elixir 中没有 Option 类型的数据结构。

Elixir 有什么?

Elixir 以 {:ok, val}{:error, reason} 的形式标记元组。现在,虽然 Elixir 理解可以与标记的元组进行模式匹配:

iex> values = [good: 1, good: 2, bad: 3, good: 4]
iex> for {:good, n} <- values, do: n * n
[1, 4, 16]

它还会忽略不匹配模式的值:

iex> values = [good: 1, good: 2, bad: 3, good: 4]
iex> for {:bananas, n} <- values, do: n * n
[]

但是,这并没有正确复制 Option 类型的行为。以下是 Scala 中的示例:

  for {
      validName  <- validateName(name)
      validEnd   <- validateEnd(end)
      validStart <- validateStart(start, end)
    } yield Event(validName, validStart, validEnd)

记住这个签名:

def validateName(name: String): Option[String]
def validateEnd(end: Int): Option[Int]
def validateStart(start: Int, end: Int): Option[Int] 

如果任何函数 return None ,完整理解表达式的结果将是 None.

使用 Elixir,错误的结果将被忽略,管道将从此愉快地继续。

问题

在这一点上,我认为将此 Option 类型实现为实现 Enumerable 协议的结构(因此它可以在 Elixir 理解中使用)应该是可能的。

但是,如果我可以使用元组模拟类似的行为,我不确定我是否愿意走这条路。

所以我有以下问题:

  1. 是否可以在 Elixir 理解中使用标记元组模拟 Option 类型?
  2. 是否有任何 Elixir 库具有可在 Elixir 推导中使用的 Monadic 类型(如我们在此处看到的那种)? (我知道 witchcraft 但他们有自己的推导式构造,目前我认为这有点矫枉过正。我对与 Elixir 的原生推导式功能一起使用的东西很感兴趣)。

回答

在搜索了 Elixir 在 hex 上的所有函数库之后,在撰写本文时 none 符合我的主要要求:

  • 可用于 Elixir 理解。

有人说 Elixir 推导式在这种情况下不够强大。这是一个可证伪的说法,所以我决定继续尝试证伪它。

Option.ex问好

是的,这个名字并不鼓舞人心。原创从来不是我的 forté。 但这是什么?

简单地说,这是 elixir 的选项类型,又名 Option/Maybe monad。是的,还有一个。

就像大多数来自 Scala/Haskell/Python 等语言的人所了解的那样,它有几个子类型 SomeNone

option.ex

defmodule Option do
  @type t(elem) :: __MODULE__.Some.t(elem) | __MODULE__.None.t()

  defmodule Some do
    @type t(elem) :: %__MODULE__{val: elem}

    defstruct [:val]

    defimpl Collectable do
      @impl Collectable
      def into(option), do: {option, fn acc, _command -> {:done, acc} end}
    end

    defimpl Enumerable do
      @impl Enumerable
      def count(_some), do: {:ok, 1}

      @impl Enumerable
      def member?(some, element), do: {:ok, some.val == element}

      @impl Enumerable
      def reduce(some, acc, fun)

      def reduce(_some, {:halt, acc}, _fun), do: {:halted, acc}
      def reduce(some, {:suspend, acc}, fun), do: {:suspended, acc, &reduce(some, &1, fun)}
      def reduce([], {:cont, acc}, _fun), do: {:done, acc}

      def reduce(%Option.Some{} = some, {:cont, acc}, fun),
        do: reduce([], fun.(some.val, acc), fun)

      @impl Enumerable
      def slice(_option), do: {:error, __MODULE__}
    end
  end

  defmodule None do
    @type t :: %__MODULE__{}

    defstruct []

    defimpl Collectable do
      @impl Collectable
      def into(option) do
        {option,
         fn
           _acc, {:cont, val} ->
             %Option.Some{val: val}

           acc, :done ->
             acc

           _acc, :halt ->
             :ok
         end}
      end
    end

    defimpl Enumerable do
      @impl Enumerable
      def count(_none), do: {:error, __MODULE__}

      @impl Enumerable
      def member?(_none, _element), do: {:error, __MODULE__}

      @impl Enumerable
      def reduce(none, acc, fun)

      def reduce(_none, {:cont, acc}, _fun), do: {:done, acc}
      def reduce(_none, {:halt, acc}, _fun), do: {:halted, acc}
      def reduce(none, {:suspend, acc}, fun), do: {:suspended, acc, &reduce(none, &1, fun)}

      @impl Enumerable
      def slice(_option), do: {:error, __MODULE__}
    end
  end

  @spec new(any) :: __MODULE__.Some.t(any)
  def new(val), do: %__MODULE__.Some{val: val}

  @spec new :: __MODULE__.None.t()
  def new, do: %__MODULE__.None{}
end

这适用于 Elixir 理解,它利用了 Optional 类型是 Functor 的事实。这意味着它的主要要求是能够被映射。通过将抽象容器转换为具体的实现细节(如 Elixir 中的列表),我能够使其工作。

如何使用?

这样做的主要目的是为 elixir 添加一个 Option 类型以与理解一起使用。所以与其他语言的比较是有用的:

在 Scala 中:

def parseShow(rawShow: String): Option[TvShow] = {
  for {
    name <- extractName(rawShow)
    yearStart <- extractYearStart(rawShow)
    yearEnd <- extractYearEnd(rawShow)
  } yield TvShow(name, yearEnd, yearStart)
}

在长生不老药中:

  @spec parse_show(String.t()) :: Option.t(TvShow.t())
  def parse_show(raw_show) do
    for name <- extract_name(raw_show),
        year_start <- extract_year_start(raw_show),
        year_end <- extract_year_end(raw_show),
        into: Option.new() do
      %TvShow{name: name, year_end: year_end, year_start: year_start}
    end
  end

您会看到,这两段代码基本相同,除了行 into: Option.new() 外,它隐含在 Scala 示例中。 Elixir 要求它是明确的,我个人也喜欢这一点。

我可以继续使用其他语言的示例,但它们读起来基本相同。这是因为大多数 FP 语言的推导式基本相同。

但这并没有回答完整的原文 post ...

标记元组中的 Elixir 等价物怎么样?

您不能使用带标签的元组来使用推导来实现相同的目的。这是不可能的。 然而,如果我们放弃理解并专注于 Elixir 的其他构造,我们可以更接近一点。

引用 Elixir 社区的另一位杰出成员@OvermindDL1 的话:

Is it possible to simulate the Option type using tagged tuples inside Elixir comprehensions?

Yes, or with with if you want an else, but you’ll want to make the tagged typed be ok: value and error: reason (which is closer to a result type, but it’s a limitation of elixir tuple lists in that they are always tuples). Traditionally {:ok, value} and :error is the “option” type in Elixir, where {:ok, value} and {:error, reason} is the “result” type in Elixir.

因此,如果您来自不同的环境,从函数式语言到 Elixir,那么这个 post 和我的 option.ex 肯定会对您有所帮助。

但是,如果您宁愿远离数学范畴和其他函数概念,就像您想要远离瘟疫一样,with 其他长生不老药的构造语句应该能很好地为您服务。

一个不比另一个好,他们有不同costs/benefits。由你决定。