Skip to content

Latest commit

 

History

History
237 lines (180 loc) · 6.69 KB

chains.md

File metadata and controls

237 lines (180 loc) · 6.69 KB

Chains

Chains are an expanded concept of . in OOP language method call syntax foo.bar(). Pangaea can describe how to deal with receiver by chains (chain context).

Chain context

Dot chain foo.bar is just one of the method chains in Pangaea. There are some kinds of chain styles, and each one shows different "context".

chain name receiver of the call
foo.bar scalar chain left-side value
foo@bar list chain each element of left-side value iterator
foo$bar reduce chain an accumulated value and each element of left-side value iterator

Scalar Chain

The receiver is left-side value, which is ordinary method chain.

10.p # 10

List Chain

The receiver is each element of left-side value iterator. This can be used as "map" or "filter" in other languages.

[1, 2, 3]@{|i| i * 2}.p # [2, 4, 6]
["foo", "var", "hoge"]@capital.p # ["Foo", "Var", "Hoge"]

In list chains, evaluated nil elements are ignored. Combining to if expression, you can filter generated elements (If).

# nil elements are ignored (because `i if i.even? == nil` if `i.even?` is false)
(1:10)@{|i| i if i.even?}.p # [2, 4, 6, 8]

Technically, _iter method of left-side value is called to obtain its iterator. Built-in objects generates its iterator respectively.

# int: 1 to self
3@{|e| e} # [1, 2, 3]
# str: each character
"abc"@{|e| e} # ["a", "b", "c"]
# range: each element within self
(1:7:2)@{|e| e} # [1, 3, 5]
# arr: each element in self
[1, 2, 3]@{|e| e} # [1, 2, 3]
# obj: each pair of self
{a: 1, b: 2}@{|e| e} # [["a", 1], ["b", 2]]
# map: each pair of self
%{'a: 1, 'b: 2}@{|e| e} # [["a", 1], ["b", 2]]

⚠️ iterator generated by _iter is nothing to do with indexing! _iter describes how to iterate over the value in loop. On the other hand, at(used for indexing) describes how to show internal structures of the value. obj[0] is usually obj._iter.next, but not always.

# Int#at returns n-th bit of self
4[0] # 0
# Int#_iter returns iterator of 1 to self
4._iter.next # 1

keys of obj's list map

Obj#_iter ignores private keys and inherited keys. This prevents chain's unexpected behaviour.

If _iter returned private keys, meta-properties not intended to be used as data would get into list chains.

# you don't be bothered by _name prop!
infixes := {_name: "infixes", add: {\1 + \2}, sub: {\1 - \2}, mul: {\1 * \2}, div: {\1 / \2}}

infixes@{|k, v| "#{k}: #{v(6, 3)}"}@p
# add: 9
# div: 2.0
# mul: 18
# sub: 3

Also, if _iter returned inherited keys, you would have to eliminate prototype's methods.

# you don't have to worry about tons of Obj's methods!
Obj.keys.p # ["A", "B", "S", "acc",...]

obj := {a: 1, b: 2, c: 3}
obj@p
# ["a", 1]
# ["b", 2]
# ["c", 3]

# on the other hand, you can access Obj's method by `at`
 obj['A].p # {|self| @{|| \}}

Reduce Chain

The receiver is each element of left-side value iterator. Also, returned value of previous call is passed to 2nd argument (In short, it's reduce!).

# reduce chain can hold initial value.
[1, 2, 3]$(0){|acc, i| acc+i} # 6
# same as above
[1, 2, 3]$(0)+ # 6

Technically, _iter method of left-side value is called to obtain its iterator.

Additional context

Additional context can be prepended by main chain context. There are 3 kinds of additional chain context(&, =, ~). Thus, there are 9 kinds (3 additional * 3 main) of context.

Lonely Chain

This chain ignores call and return nil if its receiver is nil.

# nil.capital.puts # NoPropErr: property `capital` is not defined.
nil&.capital.puts # nil

[1, 2, nil, 4]&@F.puts # [1.000000, 2.000000, 4.000000]

Thoughtful Chain

This chain returns receiver instead if returned value is nil or the call raises an error.

(1:16)~@{|i| ['fizz][i%3] + ['buzz][i%5]}.puts # [1, 2, "fizz", 4, "buzz", ..., "fizzbuzz"]

(3:20)~$([2]){|acc, n| [*acc, n] if acc.all? {|p| n % p}}.puts # [2, 3, 5, ..., 19]

# (Of course you can use built-in prime function)
20.select {.prime?}.puts # [2, 3, 5, ..., 19]

# rollback error
6~./(0) # 6

This is useful for logics like .{|x| f(x) if cond else x}. You can rewrite this to ~.{|x| f(x) if cond}.

Strict Chain

This chain keeps returned nil value. This is useful only in list context, which removes returned nil.

(1:10)@{|i| i if i.even?}.puts # [2, 4, 6, 8]
(1:10)=@{|i| i if i.even?}.puts # [nil, 2, nil, 4, nil, 6, nil, 8, nil]

Why is method call . expanded?

Because that's why Pangaea was made! A main purpose of Pangaea design is "Can chain context shorten one-liner effectively?". (In some cases, @ and $ is shorter than map, filter, and reduce)

Chain argument

Chains can take 1 argument for specific use.

Chain argument in list chain

List chain can only generate an array by default. Chain argument enables to convert generated array into specific types.

# arr by default
(?a:?d)@{|c| [c, .uc]} # [["a", "A"], ["b", "B"], ["c", "C"]]
# convert to obj
(?a:?d)@({}){|c| [c, .uc]} # {"a": "A", "b": "B", "c": "C"}
# convert to map
(?a:?d)@(%{}){|c| [c, .uc]} # %{"a": "A", "b": "B", "c": "C"}

Technically, the chain argument's digest method is called to convert the evaluated array (Metaprogramming).

Chain argument in reduce chain

Chain argument is used for an initial accumulator. If no arguments are passed, the initinal value is nil.

# initial accumulator: "initial"
(?a:?e)$("initial")+ # "initialabcd"
# initial accumulator: nil
(?a:?e)$+ # "abcd" (note that nil + "a" == "a")

Anonymous chains

If chain does not have a receiver, it uses the 1st argument of the current function instead. This is handy for property calls in methods (note that the receiver is the 1st argument of the method (Function)).

# anonymous chain in a function
# .name is same as o.name
showName := {|o| .name.p}
showName({name: "Taro"}) # Taro

# property call of a method
square := {
  side: 10,
  # .side is same as self.side
  area: m{.side ** 2},
}
square.area # 100

Why is anonymous chain introduced?

If anonymous chain were not permitted, it would be annoying that you have to write self anywhere you refer properties and call private methods.

method() is shorter than .method()!

There was another option to omit self; self properties can be referred as variables in self's methods. But this confuses properties and local variables.

# REJECTED SYNTAX
Square := {
  # self.side can be referred as side
  area: m{
    side ** 2
  },
  new: m{|side|
    # Is side a method? or a local variable?
    "side is #{side}".p
    square.bear({side: side})
  },
}