【问题标题】:Option type compatible with comprehensions in Elixr?选项类型与 Elixir 中的推导兼容?
【发布时间】:2022-02-21 19:53:00
【问题描述】:

背景

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

选择什么?

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

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

这一切都归结为 Elixir

大多数 FP 语言都有 comprehensions,Scala 和 Elixir 有 for 构造,而 Haskell 有其著名的 do 表示法。

在 Scala 和 Haskell 中,这些推导不仅适用于 Enumerables(例如 Lists),而且适用于我们的 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] 

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

使用 Elixir,糟糕的结果将被忽略,管道将从此愉快地继续下去。

问题

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

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

所以我有以下问题:

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

【问题讨论】:

  • 在我的经验中,选项类型是专门为静态类型语言实现的,它们将副作用转换为数据,我从未见过长生不老药中选项类型的必要性。 Python 很可能采用了这一点,以便他们可以更轻松地将人们迁移到他们的语言。
  • 您可以使用 Elixir 以及通过 Dialyzer(我会推荐)进行相当程度的静态输入。尽管如此,我的问题仍未得到解答。
  • 我不反对静态类型分析器,它们在某些地方很好。我讨厌看到人们在生产中实现这样的逻辑而不是使用官方方式,这样的项目不能很好地老化。 Javascript 和 Python 就是很好的例子,它们功能臃肿,却忽略了这些语言的设计目的。
  • 我尊重你的意见。让我们暂时放弃这种友好的想法交流。
  • 我不熟悉这个概念,也不知道 Elixir 的 for 构造会跳过任何在 &lt;- 左侧没有匹配的值 --我想这就像with 语句,如果没有匹配,流被重定向(可选到else 块)。这与定义 2 个函数子句(一个用于有效输入,一个用于处理“无”路径)有根本的不同吗?例如。 def option(:foo), do: "Yes"def option(_), do: nil

标签: elixir list-comprehension monads


【解决方案1】:

回答

在搜索 Elixir 的所有十六进制函数库后,在撰写本文时,没有一个符合我的主要要求:

  • 可与 Elixir 理解一起使用。

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

Option.ex打个招呼

是的,这个名字并不鼓舞人心。创意从来都不是我的强项。 但这是什么?

简单地说,这是 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)
}

在 Elixir 中:

  @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 语言中的理解基本相同。

但这并不能回答完整的原始帖子...

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

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

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

是否可以使用 Elixir 理解中的标记元组来模拟 Option 类型?

是的,或者如果你想要一个 else,或者使用 with,但你需要将标记类型设置为 ok: valueerror: reason(更接近结果类型,但它是 elixir 元组的限制列表,因为它们总是元组)。传统上,{:ok, value}:error 是 Elixir 中的“选项”类型,其中 {:ok, value}{:error, reason} 是 Elixir 中的“结果”类型。

所以,如果你来自不同的环境,从函数式语言进入 Elixir,这篇文章和我的 option.ex 肯定会对你有所帮助。

但是,如果您希望远离数学类别和其他函数概念,就像您想远离瘟疫一样,with 与其他 elixir 构造的语句应该可以很好地为您服务。

一个并不比另一个更好,它们有不同的成本/收益。这取决于你。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2022-01-11
    • 1970-01-01
    • 2015-09-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多