├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── bench └── deque_bench.exs ├── lib └── deque.ex ├── mix.exs ├── mix.lock └── test ├── deque_test.exs └── test_helper.exs /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /cover 3 | /deps 4 | /bench/snapshots 5 | erl_crash.dump 6 | *.ez 7 | .DS_Store -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: elixir 3 | git: 4 | depth: 3 5 | elixir: 6 | - 1.5.3 7 | - 1.6.5 8 | otp_release: 9 | - 20.3.8 10 | env: 11 | - MIX_ENV=test 12 | script: 13 | - mix test 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Discord 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deque 2 | 3 | [![Master](https://travis-ci.org/discordapp/deque.svg?branch=master)](https://travis-ci.org/discordapp/deque) 4 | [![Hex.pm Version](http://img.shields.io/hexpm/v/deque.svg?style=flat)](https://hex.pm/packages/deque) 5 | 6 | Erlang only supports fast prepends to lists while appending requires a full copy. Getting the size of a list is also a 7 | `O(n)` operation. This library implements a deque using two rotating lists to support fast append and prepend as well as 8 | `O(1)` size via an internal counter. 9 | 10 | ## Features 11 | 12 | - Bounded size 13 | - `Enumerable` protocol 14 | - `Collectable` protocol 15 | - `Inspect` protocol 16 | 17 | ## Usage 18 | 19 | Add it to `mix.exs` 20 | 21 | ```elixir 22 | defp deps do 23 | [{:deque, "~> 1.0"}] 24 | end 25 | ``` 26 | 27 | Then use it like other Elixir data structures. 28 | 29 | ```elixir 30 | # Deque<[3, 2, 1]> 31 | deque = 32 | Deque.new(5) 33 | |> Deque.appendleft(1) 34 | |> Deque.appendleft(2) 35 | |> Deque.appendleft(3) 36 | 37 | # Deque<[2, 1]> 38 | {3, deque} = Deque.popleft(deque) 39 | 40 | # Deque<[6, 7, 8, 9, 10]> 41 | Enum.into(0..10, Deque.new(5)) 42 | ``` 43 | 44 | ## License 45 | 46 | Deque is released under [the MIT License](LICENSE). 47 | Check [LICENSE](LICENSE) file for more information. 48 | -------------------------------------------------------------------------------- /bench/deque_bench.exs: -------------------------------------------------------------------------------- 1 | defmodule DequeBench do 2 | use Benchfella 3 | 4 | bench "Deque.new/1" do 5 | gen_deque(200, 100) 6 | :ok 7 | end 8 | 9 | bench "Enum.take_while/2 (best)", [deque: gen_deque(400, 400)] do 10 | seq = 400 11 | deque 12 | |> Enum.reverse 13 | |> Enum.take_while(&(&1 > seq)) 14 | |> Enum.reverse 15 | |> Enum.into(Deque.clear(deque)) 16 | :ok 17 | end 18 | 19 | bench "Enum.take_while/2 (average)", [deque: gen_deque(400, 400)] do 20 | seq = 300 21 | deque 22 | |> Enum.reverse 23 | |> Enum.take_while(&(&1 > seq)) 24 | |> Enum.reverse 25 | |> Enum.into(Deque.clear(deque)) 26 | :ok 27 | end 28 | 29 | bench "Enum.take_while/2 (worst)", [deque: gen_deque(400, 400)] do 30 | seq = 100 31 | deque 32 | |> Enum.reverse 33 | |> Enum.take_while(&(&1 > seq)) 34 | |> Enum.reverse 35 | |> Enum.into(Deque.clear(deque)) 36 | :ok 37 | end 38 | 39 | bench "Deque.take_while/2 (best)", [deque: gen_deque(400, 400)] do 40 | seq = 400 41 | Deque.take_while(deque, &(&1 > seq)) 42 | :ok 43 | end 44 | 45 | 46 | bench "Deque.take_while/2 (average)", [deque: gen_deque(400, 400)] do 47 | seq = 300 48 | Deque.take_while(deque, &(&1 > seq)) 49 | :ok 50 | end 51 | 52 | bench "Deque.take_while/2 (worst)", [deque: gen_deque(400, 400)] do 53 | seq = 100 54 | Deque.take_while(deque, &(&1 > seq)) 55 | :ok 56 | end 57 | 58 | defp gen_deque(n, max_size) do 59 | Enum.reduce(0..n, Deque.new(max_size), &Deque.append(&2, &1)) 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/deque.ex: -------------------------------------------------------------------------------- 1 | defmodule Deque do 2 | @moduledoc """ 3 | A fast deque implementation using 2 rotating lists. 4 | """ 5 | 6 | @opaque t :: %__MODULE__{ 7 | size: integer, 8 | max_size: integer, 9 | list1: list, 10 | list2: list, 11 | } 12 | @type value :: term 13 | 14 | defstruct size: 0, max_size: nil, list1: [], list2: [] 15 | 16 | @spec new(integer) :: t 17 | def new(max_size \\ 100) do 18 | %Deque{max_size: max_size} 19 | end 20 | 21 | @spec append(t, value) :: t 22 | def append(%Deque{size: size, max_size: max_size, list1: [], list2: list2}=deque, value) when size < max_size do 23 | %{deque | size: size + 1, list2: [value|list2]} 24 | end 25 | def append(%Deque{size: size, max_size: max_size, list2: list2}=deque, value) when size < max_size do 26 | %{deque | size: size + 1, list2: [value|list2]} 27 | end 28 | def append(%Deque{list1: [], list2: list2}=deque, value) do 29 | %{deque | list1: Enum.reverse(list2), list2: []} |> append(value) 30 | end 31 | def append(%Deque{list1: [_|list1], list2: list2}=deque, value) do 32 | %{deque | list1: list1, list2: [value|list2]} 33 | end 34 | 35 | @spec appendleft(t, value) :: t 36 | def appendleft(%Deque{size: size, max_size: max_size, list1: list1, list2: []}=deque, value) when size < max_size do 37 | %{deque | size: size + 1, list1: [value|list1]} 38 | end 39 | def appendleft(%Deque{size: size, max_size: max_size, list1: list1}=deque, value) when size < max_size do 40 | %{deque | size: size + 1, list1: [value|list1]} 41 | end 42 | def appendleft(%Deque{list1: list1, list2: []}=deque, value) do 43 | %{deque | list1: [], list2: Enum.reverse(list1)} |> appendleft(value) 44 | end 45 | def appendleft(%Deque{list1: list1, list2: [_|list2]}=deque, value) do 46 | %{deque | list1: [value|list1], list2: list2} 47 | end 48 | 49 | @spec pop(t) :: {value | nil, t} 50 | def pop(%Deque{list1: [], list2: []}=deque) do 51 | {nil, deque} 52 | end 53 | def pop(%Deque{size: size, list2: [value|list2]}=deque) do 54 | {value, %{deque | size: size - 1, list2: list2}} 55 | end 56 | def pop(%Deque{list1: list1}=deque) do 57 | %{deque | list1: [], list2: Enum.reverse(list1)} |> pop 58 | end 59 | 60 | @spec popleft(t) :: {value | nil, t} 61 | def popleft(%Deque{list1: [], list2: []}=deque) do 62 | {nil, deque} 63 | end 64 | def popleft(%Deque{size: size, list1: [value|list1]}=deque) do 65 | {value, %{deque | size: size - 1, list1: list1}} 66 | end 67 | def popleft(%Deque{list2: list2}=deque) do 68 | %{deque | list1: Enum.reverse(list2), list2: []} |> popleft 69 | end 70 | 71 | @spec last(t) :: value | nil 72 | def last(%Deque{list1: [], list2: []}), do: nil 73 | def last(%Deque{list2: [value|_]}), do: value 74 | def last(%Deque{list1: list1}=deque) do 75 | %{deque | list1: [], list2: Enum.reverse(list1)} |> last 76 | end 77 | 78 | @spec first(t) :: value | nil 79 | def first(%Deque{list1: [], list2: []}), do: nil 80 | def first(%Deque{list1: [value|_]}), do: value 81 | def first(%Deque{list2: list2}=deque) do 82 | %{deque | list1: Enum.reverse(list2), list2: []} |> first 83 | end 84 | 85 | @spec clear(t) :: t 86 | def clear(%Deque{max_size: max_size}), do: new(max_size) 87 | 88 | @spec take_while(t, (term -> boolean)) :: t 89 | def take_while(%Deque{list1: [], list2: []}=deque, _func), do: deque 90 | def take_while(%Deque{list1: list1, list2: list2}=deque, func) do 91 | case lazy_take_while(list2, func) do 92 | # If the tail list halts, then everything in head list is invalid. 93 | {:halt, list2_n, list2} -> 94 | %{deque | size: list2_n, list1: [], list2: Enum.reverse(list2)} 95 | {list2_n, list2} -> 96 | # Halting does not matter when filtering the head list. Reverse the list 97 | # before attempting to filter it, it will automatically be reversed again. 98 | {list1_n, list1} = 99 | with {:halt, list1_n, list1} <- lazy_take_while(Enum.reverse(list1), func) do 100 | {list1_n, list1} 101 | end 102 | %{deque | size: list1_n + list2_n, list1: list1, list2: Enum.reverse(list2)} 103 | end 104 | end 105 | 106 | ## Private 107 | 108 | defp lazy_take_while(list, func), do: lazy_take_while(list, [], 0, func) 109 | 110 | defp lazy_take_while([], acc, n, _func), do: {n, acc} 111 | defp lazy_take_while([h | t], acc, n, func) do 112 | if func.(h) do 113 | lazy_take_while(t, [h | acc], n + 1, func) 114 | else 115 | {:halt, n, acc} 116 | end 117 | end 118 | 119 | ## Protocols 120 | 121 | defimpl Enumerable do 122 | def reduce(_, {:halt, acc}, _fun) do 123 | {:halted, acc} 124 | end 125 | def reduce(deque, {:suspend, acc}, fun) do 126 | {:suspended, acc, &reduce(deque, &1, fun)} 127 | end 128 | def reduce(%Deque{list1: list1, list2: list2}, {:cont, acc}, fun) do 129 | reduce({list1, list2}, {:cont, acc}, fun) 130 | end 131 | def reduce({[], []}, {:cont, acc}, _fun) do 132 | {:done, acc} 133 | end 134 | def reduce({[h|list1], list2}, {:cont, acc}, fun) do 135 | reduce({list1, list2}, fun.(h, acc), fun) 136 | end 137 | def reduce({[], list2}, {:cont, acc}, fun) do 138 | reduce({Enum.reverse(list2), []}, {:cont, acc}, fun) 139 | end 140 | 141 | def member?(%Deque{list1: list1, list2: list2}, element) do 142 | {:ok, element in list1 or element in list2} 143 | end 144 | 145 | def count(%Deque{size: size}) do 146 | {:ok, size} 147 | end 148 | 149 | def slice(%Deque{}) do 150 | {:error, __MODULE__} 151 | end 152 | end 153 | 154 | defimpl Collectable do 155 | def into(original) do 156 | {original, fn 157 | deque, {:cont, value} -> Deque.append(deque, value) 158 | deque, :done -> deque 159 | _, :halt -> :ok 160 | end} 161 | end 162 | end 163 | 164 | defimpl Inspect do 165 | import Inspect.Algebra 166 | 167 | def inspect(deque, opts) do 168 | concat ["#Deque<", Inspect.List.inspect(Enum.to_list(deque), opts), ">"] 169 | end 170 | end 171 | end 172 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Deque.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :deque, 7 | version: "1.2.0", 8 | elixir: ">= 1.5.0", 9 | build_embedded: Mix.env() == :prod, 10 | start_permanent: Mix.env() == :prod, 11 | deps: deps(), 12 | package: package() 13 | ] 14 | end 15 | 16 | def application do 17 | [] 18 | end 19 | 20 | defp deps do 21 | [ 22 | {:benchfella, "~> 0.3.5", only: :dev} 23 | ] 24 | end 25 | 26 | def package do 27 | [ 28 | name: :deque, 29 | description: "Fast bounded deque using two rotating lists.", 30 | maintainers: [], 31 | licenses: ["MIT"], 32 | files: ["lib/*", "mix.exs", "README*", "LICENSE*"], 33 | links: %{ 34 | "GitHub" => "https://github.com/discordapp/deque" 35 | } 36 | ] 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "benchfella": {:hex, :benchfella, "0.3.5", "b2122c234117b3f91ed7b43b6e915e19e1ab216971154acd0a80ce0e9b8c05f5", [:mix], [], "hexpm"}, 3 | } 4 | -------------------------------------------------------------------------------- /test/deque_test.exs: -------------------------------------------------------------------------------- 1 | defmodule DequeTest do 2 | use ExUnit.Case 3 | 4 | test "new" do 5 | deque = Deque.new(5) 6 | assert deque.size == 0 7 | assert deque.max_size == 5 8 | end 9 | 10 | test "append/appendleft/pop/popleft" do 11 | deque = Enum.reduce(1..6, Deque.new(5), &Deque.append(&2, &1)) 12 | assert deque.list1 == [2, 3, 4, 5] 13 | assert deque.list2 == [6] 14 | 15 | deque = Enum.reduce(7..9, deque, &Deque.append(&2, &1)) 16 | assert deque.list1 == [5] 17 | assert deque.list2 == [9, 8, 7, 6] 18 | 19 | deque = Enum.reduce(1..3, deque, &Deque.appendleft(&2, &1)) 20 | assert deque.list1 == [3, 2, 1, 5] 21 | assert deque.list2 == [6] 22 | 23 | deque = Deque.appendleft(deque, 4) 24 | assert deque.list1 == [4, 3, 2, 1, 5] 25 | assert deque.list2 == [] 26 | 27 | {value, deque} = Deque.pop(deque) 28 | assert value == 5 29 | assert deque.list1 == [] 30 | assert deque.list2 == [1, 2, 3, 4] 31 | 32 | {value, deque} = Deque.popleft(deque) 33 | assert value == 4 34 | assert deque.list1 == [3, 2, 1] 35 | assert deque.list2 == [] 36 | end 37 | 38 | test "enumerable" do 39 | deque = Enum.reduce(1..6, Deque.new(5), &Deque.append(&2, &1)) 40 | assert Enum.to_list(deque) == [2, 3, 4, 5, 6] 41 | end 42 | 43 | test "collectable/inspect" do 44 | deque = Enum.into(1..6, Deque.new(5)) 45 | assert inspect(deque) == "#Deque<[2, 3, 4, 5, 6]>" 46 | end 47 | 48 | test "take_while" do 49 | deque = gen_take_while(498..500, 5, 498) 50 | assert Enum.to_list(deque) == [499, 500] 51 | 52 | deque = gen_take_while(400..500, 10, 492) 53 | assert Enum.to_list(deque) == [493, 494, 495, 496, 497, 498, 499, 500] 54 | end 55 | 56 | defp gen_take_while(range, max_size, max_value) do 57 | range |> Enum.into(Deque.new(max_size)) |> Deque.take_while(&(&1 > max_value)) 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------