Repository: abs-lang/abs
Pull request #349 (2020-03-28 12:24:53)

array.flatten_deep()
Flattens an array recursively until no member is an array:

```py
[[[1, 2], [[[[3]]]], [4]]].flatten_deep() # [1, 2, 3, 4]
[[1, [2, 3], 4]].flatten_deep() # [1, 2, 3, 4]
```


Patch:
diff --git a/docs/types/array.md b/docs/types/array.md
index 2f2f6d8..b25044c 100644
--- a/docs/types/array.md
+++ b/docs/types/array.md
@@ -423,6 +423,57 @@ Flattens an array a single level deep:
 [[1, 2, 3, 4]].flatten() # [1, 2, 3, 4]
 ```
 
+### flatten_deep()
+
+Flattens an array recursively until no member is an array:
+
+```py
+[[[1, 2], [[[[3]]]], [4]]].flatten_deep() # [1, 2, 3, 4]
+[[1, [2, 3], 4]].flatten_deep() # [1, 2, 3, 4]
+```
+
+### max()
+
+Finds the highest number in an array:
+
+```py
+[].max() # NULL
+[0, 5, -10, 100].max() # 100
+```
+
+### min()
+
+Finds the lowest number in an array:
+
+```py
+[].min() # NULL
+[0, 5, -10, 100].min() # -10
+```
+
+### reduce(fn, accumulator)
+
+Reduces the array to a value by iterating through its elements and applying `fn` to them:
+
+```py
+[1, 2, 3, 4].reduce(f(value, element) { return value + element }, 0) # 10
+[1, 2, 3, 4].reduce(f(value, element) { return value + element }, 10) # 20
+```
+
+
+### partition(fn)
+
+Partitions the array by applying `fn` to all of its elements
+and using the result of the function invocation as the key to partition by:
+
+```py
+f odd(n) {
+  return !!(n % 2)
+}
+
+[0, 1, 2, 3, 4, 5].partition(odd) # [[0, 2, 4], [1, 3, 5]]
+[1, "1", {}].partition(str) # [[1, "1"], [{}]]
+```
+
 ## Next
 
 That's about it for this section!
diff --git a/evaluator/functions.go b/evaluator/functions.go
index 45e83ee..f2ca697 100644
--- a/evaluator/functions.go
+++ b/evaluator/functions.go
@@ -193,6 +193,21 @@ func getFns() map[string]*object.Builtin {
 			Types: []string{object.ARRAY_OBJ},
 			Fn:    sumFn,
 		},
+		// max(array:[1, 2, 3])
+		"max": &object.Builtin{
+			Types: []string{object.ARRAY_OBJ},
+			Fn:    maxFn,
+		},
+		// min(array:[1, 2, 3])
+		"min": &object.Builtin{
+			Types: []string{object.ARRAY_OBJ},
+			Fn:    minFn,
+		},
+		// reduce(array:[1, 2, 3], f(){}, accumulator)
+		"reduce": &object.Builtin{
+			Types: []string{object.ARRAY_OBJ},
+			Fn:    reduceFn,
+		},
 		// sort(array:[1, 2, 3])
 		"sort": &object.Builtin{
 			Types: []string{object.ARRAY_OBJ},
@@ -218,11 +233,21 @@ func getFns() map[string]*object.Builtin {
 			Types: []string{object.ARRAY_OBJ},
 			Fn:    diffSymmetricFn,
 		},
-		// flattem(array:[1, 2, 3])
+		// flatten(array:[1, 2, 3])
 		"flatten": &object.Builtin{
 			Types: []string{object.ARRAY_OBJ},
 			Fn:    flattenFn,
 		},
+		// flatten(array:[1, 2, 3])
+		"flatten_deep": &object.Builtin{
+			Types: []string{object.ARRAY_OBJ},
+			Fn:    flattenDeepFn,
+		},
+		// partition(array:[1, 2, 3])
+		"partition": &object.Builtin{
+			Types: []string{object.ARRAY_OBJ},
+			Fn:    partitionFn,
+		},
 		// map(array:[1, 2, 3], function:f(x) { x + 1 })
 		"map": &object.Builtin{
 			Types: []string{object.ARRAY_OBJ},
@@ -419,7 +444,6 @@ func getFns() map[string]*object.Builtin {
 /*
 Here be the actual Builtin Functions
 */
-
 // Utility function that validates arguments passed to builtin functions.
 func validateArgs(tok token.Token, name string, args []object.Object, size int, types [][]string) object.Object {
 	if len(args) == 0 || len(args) > size || len(args) < size {
@@ -427,7 +451,7 @@ func validateArgs(tok token.Token, name string, args []object.Object, size int,
 	}
 
 	for i, t := range types {
-		if !util.Contains(t, string(args[i].Type())) {
+		if !util.Contains(t, string(args[i].Type())) && !util.Contains(t, object.ANY_OBJ) {
 			return newError(tok, "argument %d to %s(...) is not supported (got: %s, allowed: %s)", i, name, args[i].Inspect(), strings.Join(t, ", "))
 		}
 	}
@@ -1147,6 +1171,88 @@ func sumFn(tok token.Token, env *object.Environment, args ...object.Object) obje
 	return &object.Number{Token: tok, Value: sum}
 }
 
+// max(array:[1, 2, 3])
+func maxFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object {
+	err := validateArgs(tok, "max", args, 1, [][]string{{object.ARRAY_OBJ}})
+	if err != nil {
+		return err
+	}
+
+	arr := args[0].(*object.Array)
+	if arr.Empty() {
+		return object.NULL
+	}
+
+	if !arr.Homogeneous() {
+		return newError(tok, "max(...) can only be called on an homogeneous array, got %s", arr.Inspect())
+	}
+
+	if arr.Elements[0].Type() != object.NUMBER_OBJ {
+		return newError(tok, "max(...) can only be called on arrays of numbers, got %s", arr.Inspect())
+	}
+
+	max := arr.Elements[0].(*object.Number).Value
+
+	for _, v := range arr.Elements[1:] {
+		elem := v.(*object.Number)
+
+		if elem.Value > max {
+			max = elem.Value
+		}
+	}
+
+	return &object.Number{Token: tok, Value: max}
+}
+
+// min(array:[1, 2, 3])
+func minFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object {
+	err := validateArgs(tok, "min", args, 1, [][]string{{object.ARRAY_OBJ}})
+	if err != nil {
+		return err
+	}
+
+	arr := args[0].(*object.Array)
+	if arr.Empty() {
+		return object.NULL
+	}
+
+	if !arr.Homogeneous() {
+		return newError(tok, "min(...) can only be called on an homogeneous array, got %s", arr.Inspect())
+	}
+
+	if arr.Elements[0].Type() != object.NUMBER_OBJ {
+		return newError(tok, "min(...) can only be called on arrays of numbers, got %s", arr.Inspect())
+	}
+
+	min := arr.Elements[0].(*object.Number).Value
+
+	for _, v := range arr.Elements[1:] {
+		elem := v.(*object.Number)
+
+		if elem.Value < min {
+			min = elem.Value
+		}
+	}
+
+	return &object.Number{Token: tok, Value: min}
+}
+
+// reduce(array:[1, 2, 3], f(){}, accumulator)
+func reduceFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object {
+	err := validateArgs(tok, "reduce", args, 3, [][]string{{object.ARRAY_OBJ}, {object.FUNCTION_OBJ}, {object.ANY_OBJ}})
+	if err != nil {
+		return err
+	}
+
+	accumulator := args[2]
+
+	for _, v := range args[0].(*object.Array).Elements {
+		accumulator = applyFunction(tok, args[1].(*object.Function), env, []object.Object{accumulator, v})
+	}
+
+	return accumulator
+}
+
 // sort(array:[1, 2, 3])
 func sortFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object {
 	err := validateArgs(tok, "sort", args, 1, [][]string{{object.ARRAY_OBJ}})
@@ -1297,7 +1403,16 @@ func unionFn(tok token.Token, env *object.Environment, args ...object.Object) ob
 
 // flatten(array:[1, 2, 3])
 func flattenFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object {
-	err := validateArgs(tok, "flatten", args, 1, [][]string{{object.ARRAY_OBJ}})
+	return flatten("flatten", false, tok, env, args...)
+}
+
+// flatten_deep(array:[1, 2, 3])
+func flattenDeepFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object {
+	return flatten("flatten_deep", true, tok, env, args...)
+}
+
+func flatten(fnName string, deep bool, tok token.Token, env *object.Environment, args ...object.Object) object.Object {
+	err := validateArgs(tok, fnName, args, 1, [][]string{{object.ARRAY_OBJ}})
 	if err != nil {
 		return err
 	}
@@ -1308,8 +1423,12 @@ func flattenFn(tok token.Token, env *object.Environment, args ...object.Object)
 	for _, v := range originalElements {
 		switch e := v.(type) {
 		case *object.Array:
-			for _, x := range e.Elements {
-				elements = append(elements, x)
+			if deep {
+				elements = append(elements, flattenDeepFn(tok, env, e).(*object.Array).Elements...)
+			} else {
+				for _, x := range e.Elements {
+					elements = append(elements, x)
+				}
 			}
 		default:
 			elements = append(elements, e)
@@ -1319,6 +1438,49 @@ func flattenFn(tok token.Token, env *object.Environment, args ...object.Object)
 	return &object.Array{Elements: elements}
 }
 
+func partitionFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object {
+	err := validateArgs(tok, "partition", args, 2, [][]string{{object.ARRAY_OBJ}, {object.FUNCTION_OBJ, object.BUILTIN_OBJ}})
+	if err != nil {
+		return err
+	}
+
+	partitions := map[string][]object.Object{}
+	elements := args[0].(*object.Array).Elements
+	// This will allows us to preserve the order
+	// of partitions based on the order of elements.
+	//
+	// When we run the partitioning function, we store
+	// it's results in a map of result{list_of_values...}.
+	// When we loop over that map, Go doesn't guarantee
+	// order of results (https://nathanleclaire.com/blog/2014/04/27/a-surprising-feature-of-golang-that-colored-me-impressed/)
+	// but we want to, so
+	// we use the partitionOrder list to extract values
+	// from the map based on the order they were
+	// inserted in.
+	partitionOrder := []string{}
+	scanned := map[string]bool{}
+
+	for _, v := range elements {
+		res := applyFunction(tok, args[1], env, []object.Object{v})
+		eqs := object.GenerateEqualityString(res)
+
+		partitions[eqs] = append(partitions[eqs], v)
+
+		if _, ok := scanned[eqs]; !ok {
+			partitionOrder = append(partitionOrder, eqs)
+			scanned[eqs] = true
+		}
+	}
+
+	result := &object.Array{Elements: []object.Object{}}
+	for _, eqs := range partitionOrder {
+		partition := partitions[eqs]
+		result.Elements = append(result.Elements, &object.Array{Elements: partition})
+	}
+
+	return result
+}
+
 // map(array:[1, 2, 3], function:f(x) { x + 1 })
 func mapFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object {
 	err := validateArgs(tok, "map", args, 2, [][]string{{object.ARRAY_OBJ}, {object.FUNCTION_OBJ, object.BUILTIN_OBJ}})
diff --git a/object/object.go b/object/object.go
index 42f88ef..f897fb0 100644
--- a/object/object.go
+++ b/object/object.go
@@ -27,6 +27,8 @@ const (
 
 	RETURN_VALUE_OBJ = "RETURN_VALUE"
 
+	// ANY_OBJ represents any ABS type
+	ANY_OBJ      = "ANY"
 	FUNCTION_OBJ = "FUNCTION"
 	BUILTIN_OBJ  = "BUILTIN"
 


Test patch:
diff --git a/evaluator/builtin_functions_test.go b/evaluator/builtin_functions_test.go
index d3afc5f..46b960c 100644
--- a/evaluator/builtin_functions_test.go
+++ b/evaluator/builtin_functions_test.go
@@ -678,6 +678,64 @@ func TestFlatten(t *testing.T) {
 	testBuiltinFunction(tests, t)
 }
 
+func TestFlattenDeep(t *testing.T) {
+	tests := []Tests{
+		{`[1, 2, 3].flatten_deep()`, []int{1, 2, 3}},
+		{`[1, 2, [3]].flatten_deep()`, []int{1, 2, 3}},
+		{`[1, 2, [3, 4]].flatten_deep()`, []int{1, 2, 3, 4}},
+		{`[[1, 2], [3, 4]].flatten_deep()`, []int{1, 2, 3, 4}},
+		{`[[1, [2]], [3, 4]].flatten_deep()`, []int{1, 2, 3, 4}},
+		{`[[[1, [2]], [3, 4]]].flatten_deep()`, []int{1, 2, 3, 4}},
+	}
+
+	testBuiltinFunction(tests, t)
+}
+
+func TestMax(t *testing.T) {
+	tests := []Tests{
+		{`[].max()`, nil},
+		{`[-10].max()`, -10},
+		{`[-10, 0, 100, 9].max()`, 100},
+		{`[-10, 0, 100, 9, 100.1].max()`, 100.1},
+		{`[-10, {}, 100, 9].max()`, "max(...) can only be called on an homogeneous array, got [-10, {}, 100, 9]"},
+	}
+
+	testBuiltinFunction(tests, t)
+}
+
+func TestMin(t *testing.T) {
+	tests := []Tests{
+		{`[].min()`, nil},
+		{`[-10].min()`, -10},
+		{`[-10, 0, 100, 9].min()`, -10},
+		{`[-10, 0, 100, 9, -10.5].min()`, -10.5},
+		{`[-10, {}, 100, 9].min()`, "min(...) can only be called on an homogeneous array, got [-10, {}, 100, 9]"},
+	}
+
+	testBuiltinFunction(tests, t)
+}
+
+func TestReduce(t *testing.T) {
+	tests := []Tests{
+		{`[1, 2, 3, 4].reduce(f(value, element) { return value + element }, 0)`, 10},
+		{`[1, 2, 3, 4].reduce(f(value, element) { return value + element }, 10)`, 20},
+		{`[1, 2, 3, 4].reduce(f(value, element) { return value + element })`, "wrong number of arguments to reduce(...)"},
+	}
+
+	testBuiltinFunction(tests, t)
+}
+
+func TestPartition(t *testing.T) {
+	tests := []Tests{
+		{`[1, 1, 2, 2, 0].partition(f(x) { return x == 0 })[0]`, []int{1, 1, 2, 2}},
+		{`[1, 1, 2, 2, 0].partition(f(x) { return x == 0 })[1]`, []int{0}},
+		{`[1, "1"].partition(str)[0][0]`, 1},
+		{`[1, "1"].partition(str)[0][1]`, "1"},
+	}
+
+	testBuiltinFunction(tests, t)
+}
+
 func testBuiltinFunction(tests []Tests, t *testing.T) {
 	for _, tt := range tests {
 		evaluated := testEval(tt.input)