-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
19 changed files
with
1,050 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
# Used by "mix format" | ||
[ | ||
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
name: CI | ||
on: [push, pull_request] | ||
|
||
jobs: | ||
test: | ||
runs-on: ubuntu-latest | ||
name: OTP ${{matrix.otp}} / Elixir ${{matrix.elixir}} | ||
strategy: | ||
matrix: | ||
include: | ||
- elixir: "1.13" | ||
otp: "24.3" | ||
- elixir: "1.14" | ||
otp: "25.3" | ||
- elixir: "1.15" | ||
otp: "26.0" | ||
- elixir: "1.16" | ||
otp: "26.2" | ||
- elixir: "1.16" | ||
otp: "27.0-rc3" | ||
steps: | ||
- uses: actions/checkout@v2 | ||
- uses: erlef/setup-beam@v1 | ||
with: | ||
otp-version: ${{matrix.otp}} | ||
elixir-version: ${{matrix.elixir}} | ||
- name: Install Dependencies | ||
run: mix deps.get | ||
- name: Check compile warnings | ||
run: mix compile --warnings-as-errors | ||
- name: Check format | ||
run: mix format --check-formatted | ||
- name: Unit tests | ||
run: mix test.unit | ||
- name: Property-based tests | ||
run: PROP_TEST_RUNTIME=30000 mix test.prop |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
import_file("~/.iex.exs") | ||
import_if_available(Ion, only: :sigils) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
# Changelog | ||
|
||
## Dev | ||
|
||
## v0.1.0 (2024-04-25) | ||
|
||
- Initial release |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
MIT License | ||
|
||
Copyright (c) 2024 Sabiwara | ||
|
||
Permission is hereby granted, free of charge, to any person obtaining a copy of | ||
this software and associated documentation files (the "Software"), to deal in | ||
the Software without restriction, including without limitation the rights to | ||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of | ||
the Software, and to permit persons to whom the Software is furnished to do so, | ||
subject to the following conditions: | ||
|
||
The above copyright notice and this permission notice shall be included in all | ||
copies or substantial portions of the Software. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS | ||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR | ||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER | ||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN | ||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
# Ion | ||
|
||
[![Hex Version](https://img.shields.io/hexpm/v/ion.svg)](https://hex.pm/packages/ion) | ||
[![docs](https://img.shields.io/badge/docs-hexpm-blue.svg)](https://hexdocs.pm/ion/) | ||
[![CI](https://github.com/sabiwara/ion/workflows/CI/badge.svg)](https://github.com/sabiwara/ion/actions?query=workflow%3ACI) | ||
|
||
Lightweight utility library for efficient IO data and chardata handling. | ||
|
||
## TLDR | ||
|
||
Interpolation: | ||
|
||
```elixir | ||
# plain string | ||
"#{name}: #{count}." | ||
# IO data (using Ion) | ||
~i"#{name}: #{count}." | ||
# IO data (manual) | ||
[name, " ", to_string(count), ?.] | ||
``` | ||
|
||
Joining: | ||
|
||
```elixir | ||
# plain string | ||
Enum.join(integers, "+") | ||
# IO data (using Ion) | ||
Ion.join(integers, "+") | ||
# IO data (manual) | ||
Enum.map_intersperse(integers, "+", &to_string/1) | ||
``` | ||
|
||
Map joining: | ||
|
||
```elixir | ||
# plain string | ||
Enum.map_join(users, ", ", fn user -> "#{user.first}.#{user.last}@passione.org" end) | ||
# IO data (using Ion) | ||
Ion.map_join(users, ", ", fn user -> ~i"#{user.first}.#{user.last}@passione.org" end) | ||
# IO data (manual) | ||
Enum.map_intersperse(users, ", ", fn user -> [user.first, ?., user.last, "@passione.org"] end) | ||
``` | ||
|
||
## Why `Ion`? | ||
|
||
[IO data and chardata](https://hexdocs.pm/elixir/io-and-the-file-system.html#iodata-and-chardata) | ||
are one of the secret weapons behind Elixir and Erlang performance when it comes | ||
to string processing and IO. Turns out the fastest way to concatenate strings | ||
is: avoiding concatenation in the first place! | ||
|
||
While it is perfectly possible to handcraft IO data with just the standard | ||
library, it can sometimes be tedious, cryptic and error prone. `Ion` provides a | ||
few common recipes which: | ||
|
||
- drop-in replacement, with APIS consistent with the standard way of building | ||
strings | ||
- reduce the cognitive overhead and make the intent explicit | ||
- are implemented in an optimal fashion (`Ion` is fast!) | ||
- help reducing bugs through better typing (see below) | ||
|
||
The examples above illustrate how easy it is to migrate from building strings to | ||
building IO data or chardata. | ||
|
||
### Increased safety | ||
|
||
Building IO lists manually or through interspersing is error prone: we need to | ||
be careful to cast things that are neither strings nor nested IO data or will | ||
end up with invalid data: | ||
|
||
```elixir | ||
iex> as_bytes = Enum.intersperse(100..105, "+") | ||
[100, "+", 101, "+", 102, "+", 103, "+", 104, "+", 105] | ||
iex> IO.iodata_to_binary(as_bytes) | ||
"d+e+f+g+h+i" | ||
# need to make sure we have strings: | ||
iex> Enum.map_intersperse(100..105, "+", &to_string/1) |> IO.iodata_to_binary() | ||
# works just like Enum.join/2: | ||
iex> Ion.join(100..105, "+") |> IO.iodata_to_binary() | ||
"100+101+102+103+104+105" | ||
``` | ||
|
||
### Performance | ||
|
||
Because they are specialized, the join functions should also be faster than | ||
interspersing (~1.5x, see the `benchmarks` folder). | ||
|
||
## Installation | ||
|
||
Ion can be installed by adding `Ion` to your list of dependencies in `mix.exs`: | ||
|
||
```elixir | ||
def deps do | ||
[ | ||
{:ion, "~> 0.1.0"} | ||
] | ||
end | ||
``` | ||
|
||
Or you can just try it out from `iex` or an `.exs` script: | ||
|
||
```elixir | ||
iex> Mix.install([:ion]) | ||
:ok | ||
iex> Ion.join(["Hello", :world], ",") | ||
["Hello", ",", "world"] | ||
``` | ||
|
||
Documentation can be found at [https://hexdocs.pm/ion](https://hexdocs.pm/ion). | ||
|
||
## Copyright and License | ||
|
||
Ion is licensed under the [MIT License](LICENSE.md). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
defmodule Bench.IO.Empty do | ||
def inputs() do | ||
for n <- [5, 50, 500] do | ||
:rand.seed(:exrop, {1, 2, 3}) | ||
iodata = Stream.repeatedly(fn -> <<?a..?z |> Enum.random()>> end) |> Enum.take(n) | ||
{"n = #{n}", iodata} | ||
end ++ [ | ||
{"pathological", Enum.reduce(1..50, "not_empty", fn _, acc -> [acc] end)} | ||
] | ||
end | ||
|
||
def run() do | ||
Benchee.run( | ||
[ | ||
{"IO.iodata_length() == 0", fn iodata -> IO.iodata_length(iodata) == 0 end}, | ||
{"Ion.iodata_empty?/1", fn iodata -> Ion.iodata_empty?(iodata) end} | ||
], | ||
inputs: inputs(), | ||
print: [fast_warning: false] | ||
) | ||
end | ||
end | ||
|
||
Bench.IO.Empty.run() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
Operating System: macOS | ||
CPU Information: Apple M1 | ||
Number of Available Cores: 8 | ||
Available memory: 16 GB | ||
Elixir 1.14.5 | ||
Erlang 26.0.1 | ||
|
||
Benchmark suite executing with the following configuration: | ||
warmup: 2 s | ||
time: 5 s | ||
memory time: 0 ns | ||
reduction time: 0 ns | ||
parallel: 1 | ||
inputs: n = 5, n = 50, n = 500, pathological | ||
Estimated total run time: 56 s | ||
|
||
Benchmarking IO.iodata_length() == 0 with input n = 5 ... | ||
Benchmarking IO.iodata_length() == 0 with input n = 50 ... | ||
Benchmarking IO.iodata_length() == 0 with input n = 500 ... | ||
Benchmarking IO.iodata_length() == 0 with input pathological ... | ||
Benchmarking Ion.iodata_empty?/1 with input n = 5 ... | ||
Benchmarking Ion.iodata_empty?/1 with input n = 50 ... | ||
Benchmarking Ion.iodata_empty?/1 with input n = 500 ... | ||
Benchmarking Ion.iodata_empty?/1 with input pathological ... | ||
|
||
##### With input n = 5 ##### | ||
Name ips average deviation median 99th % | ||
Ion.iodata_empty?/1 60.16 M 16.62 ns ±3994.43% 16.70 ns 29.20 ns | ||
IO.iodata_length() == 0 25.62 M 39.03 ns ±7769.23% 42 ns 42 ns | ||
|
||
Comparison: | ||
Ion.iodata_empty?/1 60.16 M | ||
IO.iodata_length() == 0 25.62 M - 2.35x slower +22.41 ns | ||
|
||
##### With input n = 50 ##### | ||
Name ips average deviation median 99th % | ||
Ion.iodata_empty?/1 26.38 M 37.91 ns ±17084.78% 42 ns 42 ns | ||
IO.iodata_length() == 0 8.52 M 117.34 ns ±5382.12% 125 ns 125 ns | ||
|
||
Comparison: | ||
Ion.iodata_empty?/1 26.38 M | ||
IO.iodata_length() == 0 8.52 M - 3.10x slower +79.43 ns | ||
|
||
##### With input n = 500 ##### | ||
Name ips average deviation median 99th % | ||
Ion.iodata_empty?/1 24.78 M 40.35 ns ±41066.46% 42 ns 42 ns | ||
IO.iodata_length() == 0 1.20 M 836.14 ns ±169.27% 833 ns 916 ns | ||
|
||
Comparison: | ||
Ion.iodata_empty?/1 24.78 M | ||
IO.iodata_length() == 0 1.20 M - 20.72x slower +795.79 ns | ||
|
||
##### With input pathological ##### | ||
Name ips average deviation median 99th % | ||
IO.iodata_length() == 0 3.45 M 289.98 ns ±461.74% 292 ns 334 ns | ||
Ion.iodata_empty?/1 2.18 M 458.13 ns ±5164.45% 416 ns 542 ns | ||
|
||
Comparison: | ||
IO.iodata_length() == 0 3.45 M | ||
Ion.iodata_empty?/1 2.18 M - 1.58x slower +168.15 ns |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
range = 1..100 | ||
integers = Enum.shuffle(range) | ||
strings = Enum.map(integers, &<<&1::utf8, &1::utf8>>) | ||
charlists = Enum.map(integers, &[&1, &1]) | ||
|
||
Benchee.run( | ||
%{ | ||
"Enum.intersperse (strings)" => fn -> Enum.intersperse(strings, "-") end, | ||
"Enum.join (strings)" => fn -> Enum.join(strings, "-") end, | ||
"Ion.join (strings)" => fn -> Ion.join(strings, "-") end, | ||
"Enum.intersperse (charlists)" => fn -> Enum.intersperse(charlists, "-") end, | ||
"Ion.join (charlists)" => fn -> Ion.join(charlists, "-") end, | ||
"Enum.map_intersperse (ints)" => fn -> Enum.map_intersperse(integers, "-", &to_string/1) end, | ||
"Enum.join (ints)" => fn -> Enum.join(integers, "-") end, | ||
"Ion.join (ints)" => fn -> Ion.join(integers, "-") end, | ||
"Enum.join (range)" => fn -> Enum.join(range, "-") end, | ||
"Ion.join (range)" => fn -> Ion.join(range, "-") end, | ||
}, | ||
time: 2, | ||
memory_time: 0.5 | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
Operating System: macOS | ||
CPU Information: Apple M1 | ||
Number of Available Cores: 8 | ||
Available memory: 16 GB | ||
Elixir 1.14.5 | ||
Erlang 26.0.1 | ||
|
||
Benchmark suite executing with the following configuration: | ||
warmup: 2 s | ||
time: 2 s | ||
memory time: 500 ms | ||
reduction time: 0 ns | ||
parallel: 1 | ||
inputs: none specified | ||
Estimated total run time: 45 s | ||
|
||
Benchmarking Enum.intersperse (charlists) ... | ||
Benchmarking Enum.intersperse (strings) ... | ||
Benchmarking Enum.join (ints) ... | ||
Benchmarking Enum.join (range) ... | ||
Benchmarking Enum.join (strings) ... | ||
Benchmarking Enum.map_intersperse (ints) ... | ||
Benchmarking Ion.join (charlists) ... | ||
Benchmarking Ion.join (ints) ... | ||
Benchmarking Ion.join (range) ... | ||
Benchmarking Ion.join (strings) ... | ||
|
||
Name ips average deviation median 99th % | ||
Ion.join (strings) 1840.60 K 0.54 μs ±2872.63% 0.50 μs 0.67 μs | ||
Ion.join (charlists) 1763.59 K 0.57 μs ±3011.09% 0.50 μs 0.71 μs | ||
Enum.intersperse (charlists) 1219.01 K 0.82 μs ±1589.04% 0.75 μs 0.96 μs | ||
Enum.intersperse (strings) 1217.86 K 0.82 μs ±1577.94% 0.75 μs 0.96 μs | ||
Enum.join (strings) 498.47 K 2.01 μs ±411.72% 1.88 μs 3.17 μs | ||
Ion.join (ints) 297.30 K 3.36 μs ±139.02% 3.25 μs 4.21 μs | ||
Ion.join (range) 280.18 K 3.57 μs ±162.61% 3.46 μs 4.50 μs | ||
Enum.map_intersperse (ints) 265.88 K 3.76 μs ±162.12% 3.67 μs 4.63 μs | ||
Enum.join (ints) 209.26 K 4.78 μs ±69.28% 4.63 μs 8.54 μs | ||
Enum.join (range) 190.09 K 5.26 μs ±121.90% 5.13 μs 7.33 μs | ||
|
||
Comparison: | ||
Ion.join (strings) 1840.60 K | ||
Ion.join (charlists) 1763.59 K - 1.04x slower +0.0237 μs | ||
Enum.intersperse (charlists) 1219.01 K - 1.51x slower +0.28 μs | ||
Enum.intersperse (strings) 1217.86 K - 1.51x slower +0.28 μs | ||
Enum.join (strings) 498.47 K - 3.69x slower +1.46 μs | ||
Ion.join (ints) 297.30 K - 6.19x slower +2.82 μs | ||
Ion.join (range) 280.18 K - 6.57x slower +3.03 μs | ||
Enum.map_intersperse (ints) 265.88 K - 6.92x slower +3.22 μs | ||
Enum.join (ints) 209.26 K - 8.80x slower +4.24 μs | ||
Enum.join (range) 190.09 K - 9.68x slower +4.72 μs | ||
|
||
Memory usage statistics: | ||
|
||
Name Memory usage | ||
Ion.join (strings) 3.13 KB | ||
Ion.join (charlists) 3.13 KB - 1.00x memory usage +0 KB | ||
Enum.intersperse (charlists) 3.09 KB - 0.99x memory usage -0.03906 KB | ||
Enum.intersperse (strings) 3.09 KB - 0.99x memory usage -0.03906 KB | ||
Enum.join (strings) 4.66 KB - 1.49x memory usage +1.52 KB | ||
Ion.join (ints) 5.48 KB - 1.75x memory usage +2.34 KB | ||
Ion.join (range) 5.52 KB - 1.76x memory usage +2.39 KB | ||
Enum.map_intersperse (ints) 5.45 KB - 1.74x memory usage +2.32 KB | ||
Enum.join (ints) 5.53 KB - 1.77x memory usage +2.40 KB | ||
Enum.join (range) 5.53 KB - 1.77x memory usage +2.40 KB | ||
|
||
**All measurements for memory usage were the same** |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
range = 1..100 | ||
integers = Enum.shuffle(range) | ||
strings = Enum.map(integers, &<<&1::utf8, &1::utf8>>) | ||
charlists = Enum.map(integers, &[&1, &1]) | ||
|
||
Benchee.run( | ||
%{ | ||
"Enum.map_intersperse (strings)" => fn -> Enum.map_intersperse(strings, "-", & &1) end, | ||
"Enum.map_join (strings)" => fn -> Enum.map_join(strings, "-", & &1) end, | ||
"Ion.map_join (strings)" => fn -> Ion.map_join(strings, "-", & &1) end, | ||
"Enum.map_intersperse (charlists)" => fn -> Enum.map_intersperse(charlists, "-", & &1) end, | ||
"Ion.map_join (charlists)" => fn -> Ion.map_join(charlists, "-", & &1) end, | ||
"Enum.map_intersperse (ints)" => fn -> Enum.map_intersperse(integers, "-", &to_string/1) end, | ||
"Enum.map_join (ints)" => fn -> Enum.map_join(integers, "-", & &1) end, | ||
"Ion.map_join (ints)" => fn -> Ion.map_join(integers, "-", & &1) end, | ||
"Enum.map_join (range)" => fn -> Enum.map_join(range, "-", & &1) end, | ||
"Ion.map_join (range)" => fn -> Ion.map_join(range, "-", & &1) end, | ||
}, | ||
time: 2, | ||
memory_time: 0.5 | ||
) |
Oops, something went wrong.