【问题标题】:Elixir, using function from another moduleElixir,使用另一个模块的函数
【发布时间】:2019-05-02 16:39:35
【问题描述】:

我对编程和灵丹妙药非常陌生。所以我很高兴能尽可能多地学习。但是我遇到了麻烦。我正在寻找如何在另一个模块中使用我的功能的方法。我正在构建将键值映射存储在内存中的 Web 服务器。为了保持地图的临时性,我决定使用代理。这是我的代码的一部分:

defmodule Storage do
  use Agent

  def start_link do
    Agent.start_link(fn -> %{} end, name: :tmp_storage)
  end

  def set(key, value) do
    Agent.update(:tmp_storage, fn map -> Map.put_new(map, key, value) end)
  end

  def get(key) do
    Agent.get(:tmp_storage, fn map -> Map.get(map, key) end)
  end
end

所以我试图把这个函数放到网络服务器的路由中:

defmodule Storage_router do
  use Plug.Router
  use Plug.Debugger
  require Logger
  plug(Plug.Logger, log: :debug)
  plug(:match)
  plug(:dispatch)

  post "/storage/set" do
    with {:ok, _} <- Storage.set(key, value) do
      send_resp(conn, 200, "getting the value")
    else
      _ ->
        send_resp(conn, 404, "nothing")
    end
  end
end

我收到了:

warning: variable "key" does not exist and is being expanded to "key()", please use parentheses to remove the ambiguity or change the variable name lib/storage_route.ex:12

warning: variable "value" does not exist and is being expanded to "value()", please use parentheses to remove the ambiguity or change the variable name lib/storage_route.ex:12

寻找任何建议\帮助

【问题讨论】:

  • 我对 mix.exs 进行了更改,并在文件下方解释了更改。

标签: function elixir


【解决方案1】:

我不完全确定您要完成什么,但错误告诉您传递给路由器 with 语句的 keyvalue 未定义。 Elixir 认为您正在尝试使用这些参数调用函数,因为它们没有“绑定”到某个值。这就是为什么你看到warning: variable "value" does not exist and is being expanded to "value()"

我想这不是一个真正的答案,但可能更多的是对您所看到的错误的解释。

【讨论】:

    【解决方案2】:

    我对编程和长生不老药非常陌生。

    我认为从 elixir 开始学习编程并不明智。我会从 python 或 ruby​​ 开始,然后一两年后我会尝试 elixir。

    您需要学习的第一件事是如何发布代码。在 google 上搜索如何在 stackoverflow 上发布代码。然后,你必须让你的缩进排成一行。您在使用计算机编程文本编辑器吗?如果没有,那么你必须得到一个。有很多免费的。我使用 vim,它安装在 Unix 之类的计算机上。您可以通过在终端窗口中输入 vimtutor 来学习如何使用 vim。

    接下来,您的代码中有语法错误:

     Agent.start_link(fn -> %{} end, name: :tmp_storage
        end)  
    

    应该是:

     Agent.start_link(fn -> %{} end, name: :tmp_storage)
    

    您收到的警告是因为您的代码尝试执行以下操作:

    def show do
       IO.puts x
    end
    

    Elixir 和其他阅读该代码的人都会问:“x 到底是什么?”变量 x 永远不会在任何地方赋值,因此变量 x 不存在,并且您不能输出不存在的东西。你在这里做同样的事情:

       with {:ok, _} <- Storage.set(key, value) do
         send_resp(conn, 200, "getting the value")
       else
         _->
          send_resp(conn, 404, "nothing")
       end
    

    你调用函数:

    Storage.set(key, value)
    

    但变量 keyvalue 从未被赋值,elixir(以及其他阅读该代码的人)想知道,“键和值到底是什么?”

    这是函数的工作方式:

    b.ex:

    defmodule MyFuncs do
      def show(x, y) do
        IO.puts x
        IO.puts y
      end
    end
    
    defmodule MyWeb do
      def go do
        height = 10
        width = 20
    
        MyFuncs.show(height, width)
      end
    end
    

    在 iex 中:

    ~/elixir_programs$ iex b.ex
    Erlang/OTP 20 [erts-9.3] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:10] [hipe] [kernel-poll:false]
    
    Interactive Elixir (1.6.6) - press Ctrl+C to exit (type h() ENTER for help)
    
    iex(1)> MyWeb.go
    10
    20
    :ok
    iex(2)> 
    

    因此,在您的代码中,您需要编写如下内容:

    post "/storage/set" do
      key = "hello"
      value = 10
    
      with {:ok, _} <- Storage.set(key, value) do
        send_resp(conn, 200, "Server saved the key and value.")
      else
        _->
          send_resp(conn, 404, "nothing")
      end
    end
    

    但是,这将为每个发布请求存储相同的键/值。大概,您想存储发送请求正文中发送的任何内容。你知道get请求和post请求的区别吗? get 请求将数据附加到 url 的末尾,而 post 请求在“请求正文”中发送数据,因此根据请求的类型,提取数据的过程也不同。

    你在读什么教程?本教程:https://www.jungledisk.com/blog/2018/03/19/tutorial-a-simple-http-server-in-elixir/,向您展示如何从发布请求的正文中提取数据。发布请求正文中的数据只是一个字符串。如果字符串是 JSON 格式,那么您可以使用 Poison.decode!() 将字符串转换为 elixir 映射,这样您就可以轻松提取与您感兴趣的键关联的值。例如:

      post "/storage/set" do
        {:ok, body_string, conn} = read_body(conn)
        body_map = Poison.decode!(body_string)
    
        IO.inspect(body_map) #This outputs to terminal window where server is running 
    
        message = get_in(body_map, ["message"])    
        send_resp(
          conn, 
          201,
          "Server received: #{message}\n"
        )
      end
    

    然后您可以在另一个终端窗口中使用以下 curl 命令向该路由发送 post 请求:

    $ curl -v -H 'Content-Type: application/json' "http://localhost:8085/storage/set" -d '{"message": "hello world" }'
    

    -v => 详细输出,-H => 请求标头,-d => 数据)

    现在,根据我所说的您上面的代码有问题,您应该想知道这一行:

    {:ok, body_string, conn} = read_body(conn)
    

    那条线路调用:

    read_body(conn)
    

    但是变量conn 没有在任何地方赋值。但是,Plug 不可见地创建了 conn 变量并为其赋值。

    这是一个使用 Agent 存储发布请求数据的完整示例(按照我上面链接的教程):

    simple_server
       config/
       lib/
           simple_server/
               application.ex
               router.ex
               storage.ex
       test/
    

    elixir 的约定是在lib/ 目录中有一个与您的项目同名的目录,在本例中为 simple_server,然后您为您定义的模块提供反映目录结构的名称。因此,在 router.ex 中您将定义一个名为 SimpleServer.Router 的模块,在 storage.ex 中您将定义一个名为 SimpleServer.Storage 的模块。但是,模块名称中的. 对elixir 没有什么特别的意义,因此如果您决定在文件lib/rocks.ex 中将模块命名为F.R.O.G.S,您将不会收到错误消息——并且您的代码可以正常工作。

    router.ex:

    defmodule SimpleServer.Router do
      use Plug.Router
      use Plug.Debugger
    
      require Logger
    
      plug(Plug.Logger, log: :debug)
      plug(:match)
      plug(:dispatch)
    
      get "/storage/:key" do
        resp_msg = case SimpleServer.Storage.get(key) do
          nil -> "The key #{key} doesn't exist!\n"
          val -> "The key #{key} has value #{val}.\n"
        end
    
        send_resp(conn, 200, resp_msg)
      end
    
      post "/storage/set" do
        {:ok, body_string, conn} = read_body(conn)
        body_map = Poison.decode!(body_string)
    
        IO.inspect(body_map) #This outputs to terminal window where server is running 
    
        Enum.each(
          body_map, 
          fn {key, val} -> SimpleServer.Storage.set(key,val) end
        )
    
        send_resp(
          conn, 
          201,
          "Server stored all key-value pairs\n"
        )
      end
    
      match _ do
        send_resp(conn, 404, "not found")
      end
    
    
    end
    

    上面代码中首先要注意的是路由:

    get "/storage/:key" do
    

    这将匹配如下路径:

    /storage/x 
    

    plug 将创建一个名为 key 的变量并将其赋值为“x”,如下所示:

     key = "x"
    

    另外,请注意,当您调用函数时:

    width = 10
    height = 20
    show(width, height)
    

    elixir看函数定义:

    def show(x, y) do
      IO.puts x
      IO.puts y
    end
    

    并像这样匹配对 def 的函数调用:

        show(width, height)
              |       |
              V       V
    def show( x    ,  y) do
      ...
    end
    

    并执行任务:

     x = width
     y = height
    

    然后,您可以在函数内部使用 x 和 y 变量。在这一行:

        Enum.each(
          body_map, 
    
          #  | | | | |
          #  V V V V V
    
          fn {key, val} -> SimpleServer.Storage.set(key,val) end
        )
    

    Elixir 将调用匿名函数为keyval 传递值,如下所示:

    func("x", "10")
    

    因此,在匿名函数的主体中,您可以使用变量keyval

    SimpleServer.Storage.set(key,val)
    

    因为变量 keyval 已经被赋值了。

    storage.ex:

    defmodule SimpleServer.Storage do
      use Agent
    
      def start_link(_args) do  #<*** Note the change here
        Agent.start_link(fn -> %{} end, name: :tmp_storage)
      end
    
      def set(key, value) do
        Agent.update(
          :tmp_storage, 
          fn(map) -> Map.put_new(map, key, value) end
        )
      end
    
      def get(key) do
        Agent.get(
          :tmp_storage, 
          fn(map) -> Map.get(map, key) end
        )
      end
    
    end
    

    应用程序.ex:

    defmodule SimpleServer.Application do
      # See https://hexdocs.pm/elixir/Application.html
      # for more information on OTP Applications
      @moduledoc false
    
      use Application
    
      def start(_type, _args) do
        # List all child processes to be supervised
        children = [
          Plug.Adapters.Cowboy.child_spec(scheme: :http, plug: SimpleServer.Router, options: [port: 8085]),
    
          {SimpleServer.Storage, []}
        ]
    
        # See https://hexdocs.pm/elixir/Supervisor.html
        # for other strategies and supported options
        opts = [strategy: :one_for_one, name: SimpleServer.Supervisor]
        Supervisor.start_link(children, opts)
      end
    end
    

    mix.exs:

    defmodule SimpleServer.MixProject do
      use Mix.Project
    
      def project do
        [
          app: :simple_server,
          version: "0.1.0",
          elixir: "~> 1.6",
          start_permanent: Mix.env() == :prod,
          deps: deps()
        ]
      end
    
      # Run "mix help compile.app" to learn about applications.
      def application do
        [
          extra_applications: [:logger],
          mod: {SimpleServer.Application, []}
        ]
      end
    
    
      # Run "mix help deps" to learn about dependencies.
      defp deps do
        [
            {:poison, "~> 4.0"},
            {:plug_cowboy, "~> 2.0"}
    
          # {:dep_from_hexpm, "~> 0.3.0"},
          # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"},
        ]
      end
    end
    

    注意,如果您使用教程中指定的依赖项和版本,您会收到一些警告,包括警告:

    ~/elixir_programs/simple_server$ iex -S mix
    ...
    ...
    
    12:48:57.767 [warn]  Setting Ranch options together 
    with socket options is deprecated. Please use the new
    map syntax that allows specifying socket options 
    separately from other options.
    

    ...这是 Plug 的问题。以下是我用来摆脱所有警告的依赖项和版本:

       {:poison, "~> 4.0"},
       {:plug_cowboy, "~> 2.0"}
    

    此外,当您将应用程序列为依赖项时,您不再需要在 :extra_applications 列表中输入它。 Elixir 将在启动您的应用程序之前自动启动所有列为依赖项的应用程序。见:applications v. :extra_applications

    服务器启动后,您可以使用另一个终端窗口发送带有curl 的发布请求(或者您可以使用其他程序):

    ~$  curl -v -H 'Content-Type: application/json' "http://localhost:8085/storage/set" -d '{"x": "10", "y": "20" }
    
    *   Trying 127.0.0.1...
    * TCP_NODELAY set
    * Connected to localhost (127.0.0.1) port 8085 (#0)
    > POST /storage/set HTTP/1.1
    > Host: localhost:8085
    > User-Agent: curl/7.58.0
    > Accept: */*
    > Content-Type: application/json
    > Content-Length: 23
    > 
    * upload completely sent off: 23 out of 23 bytes
    < HTTP/1.1 201 Created
    < server: Cowboy
    < date: Fri, 30 Nov 2018 19:22:23 GMT
    < content-length: 34
    < cache-control: max-age=0, private, must-revalidate
    < 
    Server stored all key-value pairs
    * Connection #0 to host localhost left intact
    

    &gt; 行是请求,&lt; 行是响应。另外,在服务器运行的终端窗口中检查输出。

    ~$  curl -v http://localhost:8085/storage/z
    
    *   Trying 127.0.0.1...
    * TCP_NODELAY set
    * Connected to localhost (127.0.0.1) port 8085 (#0)
    > GET /storage/z HTTP/1.1
    > Host: localhost:8085
    > User-Agent: curl/7.58.0
    > Accept: */*
    > 
    < HTTP/1.1 200 OK
    < server: Cowboy
    < date: Fri, 30 Nov 2018 19:22:30 GMT
    < content-length: 25
    < cache-control: max-age=0, private, must-revalidate
    < 
    The key z doesn't exist!
    * Connection #0 to host localhost left intact
    

    .

    ~$  curl -v http://localhost:8085/storage/x
    
    *   Trying 127.0.0.1...
    * TCP_NODELAY set
    * Connected to localhost (127.0.0.1) port 8085 (#0)
    > GET /storage/x HTTP/1.1
    > Host: localhost:8085
    > User-Agent: curl/7.58.0
    > Accept: */*
    > 
    < HTTP/1.1 200 OK
    < server: Cowboy
    < date: Fri, 30 Nov 2018 19:22:37 GMT
    < content-length: 24
    < cache-control: max-age=0, private, must-revalidate
    < 
    The key x has value 10.
    * Connection #0 to host localhost left intact
    

    【讨论】:

      【解决方案3】:

      您需要从 %Plug.Conn{} 对象 (conn) 中提取键/值参数。尚未在您的路由范围内定义键/值变量。 conn 对象之所以可用,是因为它是由 Plug 提供的post 宏注入的。

      我不太清楚您向路由器提交的请求类型,但我假设它是 JSON 作为示例。您可以通过执行以下操作手动解析连接中的正文:

      with {:ok, raw_body} <- Plug.Conn.read_body(conn),
           {:ok, body} <- Poison.decode(raw_body) do
        key = Map.get(body, "key")
        value = map.get(body, "value")
        # ... other logic
      end
      

      但是,Plug 项目为您提供了一个非常方便的插件,可以以通用方式解析请求正文:Plug.Parsers

      要在您的路由器中实现此功能,您只需将插件添加到路由器顶部(我认为是 Plug.Logger 下方):

      plug Plug.Parsers, 
        parsers: [:urlencoded, :json]
        json_decoder: Poison,
        pass: ["text/*", "application/json"]
      

      :urlencoded 部分将解析您的查询参数,:json 部分将解析请求正文。

      然后在您的路线下方,您可以从:params 键中的conn 对象中获取键/值参数,如下所示:

      %{params: params} = conn
      key = Map.get(params, "key")
      value = Map.get(params, "value")
      

      另外,我应该指出,目前最好的 JSON 解码器是 Jason,它基本上是 Poison 的直接替代品,但速度更快。

      无论如何,阅读 hexdocs 确实有助于弄清楚这些东西,而且 Plug 项目有很好的文档。我认为 Elixir 是一门很棒的编程语言(尽管学习面向对象的范式也很重要)。编码愉快!

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-09-14
        • 1970-01-01
        • 1970-01-01
        • 2018-04-04
        • 2016-05-31
        相关资源
        最近更新 更多