Do you want to pick up from where you left of?
Take me there

Enum

مجموعه‌ای از الگوریتم‌ها برای برشمردن شمارشی‌ها.

بررسی کوتاه

ماژول Enum در برگیرنده‌ی بیش از ۷۰ تابع برای کار با شمارشی‌ها^1 است. همه‌ی مجموعه‌هایی که در درس پیش previous lesson، درباره‌ی آنها آموختیم، به جز چندتایی‌ها، شمارشی هستند.

این درس تنها بخشی از توابع موجود را پوشش می‌دهد، گرچه خودمان می‌توانیم آنها را امتحان کنیم. بیایید آزمایش کوچکی در IEx انجام دهیم.

iex> Enum.__info__(:functions) |> Enum.each(fn({function, arity}) ->
...>   IO.puts "#{function}/#{arity}"
...> end)
all?/1
all?/2
any?/1
any?/2
at/2
at/3
...

آشکار است که قابلیت‌های زیادی در دسترس داریم، و این دلیل خوبی دارد. شمارش در مرکز برنامه‌نویسی تابعی قرار دارد، و این می‌تواند در کنار سایر امتیازات الکسیر قدرت بسیاری به توسعه‌دهندگان بدهد.

توابع متداول

فهرست کامل توابع در مستندات رسمی به نشانی Enum در دسترس است، برای شمارش کندرو[^2] از ماژول Stream استفاده کنید.

all?

در هنگام استفاده از all?/2، و بسیاری توابع دیگر از ماژول Enum، تابعی را به عنوان ورودی استفاده می‌کنیم تا روی عناصر مجموعه اعمال شود. در مورد all?/2، کل مجموعه باید به true ارزیابی شود در غیر این صورت false بازگردانده خواهد شد:

iex> Enum.all?(["foo", "bar", "hello"], fn(s) -> String.length(s) == 3 end)
false
iex> Enum.all?(["foo", "bar", "hello"], fn(s) -> String.length(s) > 1 end)
true

any?

بر خلاف مثال بالا، اگر دست کم یک عنصر به true ارزیابی شود مقدار بازگشتی any?/2 برابر با true خواهد بود:

iex> Enum.any?(["foo", "bar", "hello"], fn(s) -> String.length(s) == 5 end)
true

chunk_every

اگر نیاز دارید مجموعه‌ای را به گروه‌های کوچکتر تقسیم کنید، احتمالا در پی تابع chunk_every/2 هستید:

iex> Enum.chunk_every([1, 2, 3, 4, 5, 6], 2)
[[1, 2], [3, 4], [5, 6]]

چند گزینه برای chunk_every/4 وجود دارد اما به آنها نمی‌پردازیم، مستندات رسمی تابع را بررسی کنید تا بیشتر در این مورد بدانید.

chunk_by

اگر نیاز به گروه‌بندی مجموعه بر اساس معیاری جز اندازه داشته باشیم، می‌توانیم از تابع chunk_by/2 استفاده کنیم. این تابع یک شمارشی و یک تابع را به عنوان ورودی دریافت می‌کند، و هنگامی که مقدار بازگشتی آن تابع تغییر کند گروه جدیدی آغاز می‌شود و ساخت گروه بعدی شروع می‌شود. در مثال پایین، رشته‌های هم اندازه در یک گروه قرار می‌گیرند تا هنگامی که به رشته‌ای با طول جدید برخورد کنیم:

iex> Enum.chunk_by(["one", "two", "three", "four", "five"], fn(x) -> String.length(x) end)
[["one", "two"], ["three"], ["four", "five"]]
iex> Enum.chunk_by(["one", "two", "three", "four", "five", "six"], fn(x) -> String.length(x) end)
[["one", "two"], ["three"], ["four", "five"], ["six"]]

map_every

گاهی تکه کردن مجموعه برای کاری که نیاز داریم کافی نیست. در این صورت، map_every/3 ممکن برای انجام کاری روی nth‍‍‍‍‍‍‍ عنصر مفید باشد، همواره شامل اولین عضو هم می‌شود:

# Apply function every three items
iex> Enum.map_every([1, 2, 3, 4, 5, 6, 7, 8], 3, fn x -> x + 1000 end)
[1001, 2, 3, 1004, 5, 6, 1007, 8]

each

ممکن است نیاز به پیمایش یک مجموعه باشد اما بدون تولید کردن مقداری جدید، برای این کار از each/2 استفاده می‌کنیم:

iex> Enum.each(["one", "two", "three"], fn(s) -> IO.puts(s) end)
one
two
three
:ok

نکته: تابع each/2 اتم :ok را باز می‌گرداند.

map

برای اعمال تابع خود به همه‌ی عناصر یک مجموعه و تولید مجموعه‌ای جدید از تابع map/2 استفاده می‌کنیم:

iex> Enum.map([0, 1, 2, 3], fn(x) -> x - 1 end)
[-1, 0, 1, 2]

min

تابع min/1 کمترین مقدار مجموعه را می‌یابد:

iex> Enum.min([5, 3, 0, -1])
-1

تابع min/2 همان کار را می کند، اما اجازه می‌دهد تابعی را مشخص کنیم تا در صورت خالی بودن مجموعه از آن برای تولید مقدار کمینه استفاده شود.

iex> Enum.min([], fn -> :foo end)
:foo

max

تابع max/1 مقدار بیشینه‌ی مجموعه را بازمی‌گرداند:

iex> Enum.max([5, 3, 0, -1])
5

تابع max/2 برای max/1 همان نقشی را دارد که min/2 برای min/1 دارد:

iex> Enum.max([], fn -> :bar end)
:bar

filter

تابع filter/2 این امکان را به ما می‌دهد که مجموعه را پالایش کنیم تا تنها شامل عناصری شود که تابعی که به عنوان ورودی به آن داده‌ایم آنها را به true‍ ارزیابی کند.

iex> Enum.filter([1, 2, 3, 4], fn(x) -> rem(x, 2) == 0 end)
[2, 4]

reduce

با استفاده از reduce/3 می‌توانیم یک مجموعه را به یک مقدار خلاصه کنیم. برای این کار یک انباشتگر اختیاری (10 در این مثال) به تابع خود می‌فرستیم؛ اگر انباشتگری تعیین نشود اولین عنصر نوع‌داده‌ی شمارشی استفاده خواهد شد:

iex> Enum.reduce([1, 2, 3], 10, fn(x, acc) -> x + acc end)
16

iex> Enum.reduce([1, 2, 3], fn(x, acc) -> x + acc end)
6

iex> Enum.reduce(["a","b","c"], "1", fn(x,acc)-> x <> acc end)
"cba1"

sort

وجود دو تابع مرتب‌‌سازی مجموعه‌ها را ساده کرده است.

تابع sort/1 از مرتب‌سازی عبارت ارلنگ برای تعیین توالی ترتیب استفاده می‌کند:

iex> Enum.sort([5, 6, 1, 3, -1, 4])
[-1, 1, 3, 4, 5, 6]

iex> Enum.sort([:foo, "bar", Enum, -1, 4])
[-1, 4, Enum, :foo, "bar"]

برای ساده‌تر کردن کار، در حالی که تابع sort/2 به ما اجازه می‌دهد تابع مرتب‌سازی خودمان را استفاده کنیم:

# with our function
iex> Enum.sort([%{:val => 4}, %{:val => 1}], fn(x, y) -> x[:val] > y[:val] end)
[%{val: 4}, %{val: 1}]

# without
iex> Enum.sort([%{:count => 4}, %{:count => 1}])
[%{count: 1}, %{count: 4}]

تابع sort/2 این امکان را می‌دهد که :asc یا :desc را به عنوان تابع مرتب‌سازی استفاده کنیم:

Enum.sort([2, 3, 1], :desc)
[3, 2, 1]

uniq

می‌توانیم از uniq/1 برای حذف موارد تکراری از انواع داده‌ی شمارشی‌ استفاده کنیم:

iex> Enum.uniq([1, 2, 3, 2, 1, 1, 1, 1, 1])
[1, 2, 3]

uniq_by

تابع uniq_by/2 هم موارد تکراری را حذف می‌کند، اما اجازه می‌دهد تابعی را برای مقایسه‌ی یکتایی به آن معرفی کنیم:

iex> Enum.uniq_by([%{x: 1, y: 1}, %{x: 2, y: 1}, %{x: 3, y: 3}], fn coord -> coord.y end)
[%{x: 1, y: 1}, %{x: 3, y: 3}]

استفاده از عملگر ضبط (&) با Enum

بسیاری از توابع در ماژول Enum الکسیر توابع ناشناس را به عنوان نشانوند^3 می‌پذیرند تا روی هر عضو نوع داده‌ی شمارشی کار کنند.

این توابع ناشناس معمولا به صورت کوتاه با استفاده از عملگر ضبط^4 (&) نوشته می‌شوند.

اکنون چندین نمونه از شیوه‌ی کاربرد عملگر ضبط و ماژول Enum را خواهیم دید. نسخه‌های مختلف از لحاظ عملکرد معادل هستند.

استفاده از عملگر ضبط با یک تابع ناشناس

در زیر مثالی از قواعد دستوری ارسال یک تابع ناشناس به Enum.map/2 آمده است.

iex> Enum.map([1,2,3], fn number -> number + 3 end)
[4, 5, 6]

حال از عملگر ضبط (&) استفاده می‌کنیم؛ هر عضو از لیست اعداد ([1,2,3]) ضبط می‌شود و در هنگام ارسال به تابع نگاشت، به متغیر &1 تخصیص می‌یابد.

iex> Enum.map([1,2,3], &(&1 + 3))
[4, 5, 6]

می‌توان بازطراحی را ادامه داد و تابع ناشناس استفاده کننده از عملگر ضبط را به یک متغیر تخصیص داد و سپس آن را در تابع Enum.map/2 استفاده کرد.

iex> plus_three = &(&1 + 3)
iex> Enum.map([1,2,3], plus_three)
[4, 5, 6]

استفاده از عملگر ضبط به همراه یک تابع نامیده شده

نخست یک تابع نامیده شده می‌سازیم و آن را درون تابع ناشناس تعریف شده در Enum.map/2 فراخوانی می‌کنیم.

defmodule Adding do
  def plus_three(number), do: number + 3
end

iex>  Enum.map([1,2,3], fn number -> Adding.plus_three(number) end)
[4, 5, 6]

سپس می‌توانیم با استفاده از عملگر ضبط، بازطراحی کنیم.

iex> Enum.map([1,2,3], &Adding.plus_three(&1))
[4, 5, 6]

برای کوتاه‌سازی بیشتر، می‌توانیم تابع نامیده شده را مستقیما فراخوانی کنیم بی آن که به صورت صریح متغیر را ضبط کنیم.

iex> Enum.map([1,2,3], &Adding.plus_three/1)
[4, 5, 6]

[^2]: Lazy Enumeration

Caught a mistake or want to contribute to the lesson? Edit this lesson on GitHub!