Databricksにおける高階関数のイントロダクション

以下の[高階関数(higher order function)](https://qiita.com/taka_yayoi/items/6efefabe544a25678b40)のサンプルノートブックをウォークスルーします。

https://github.com/taka-yayoi/public_repo/blob/main/higher_order_function/higher-order-functions.sql

# ネストされたデータを持つテーブルの作成

テーブルとは言っていますが、実際には一時ビューです。

```sql:SQL
CREATE
OR REPLACE TEMPORARY VIEW nested_data AS
SELECT
  id AS key,
  ARRAY(
    CAST(RAND(1) * 100 AS INT),
    CAST(RAND(2) * 100 AS INT),
    CAST(RAND(3) * 100 AS INT),
    CAST(RAND(4) * 100 AS INT),
    CAST(RAND(5) * 100 AS INT)
  ) AS values,
  ARRAY(
    ARRAY(
      CAST(RAND(1) * 100 AS INT),
      CAST(RAND(2) * 100 AS INT)
    ),
    ARRAY(
      CAST(RAND(3) * 100 AS INT),
      CAST(RAND(4) * 100 AS INT),
      CAST(RAND(5) * 100 AS INT)
    )
  ) AS nested_values
FROM
  range(5)
```

```sql:SQL
SELECT * FROM nested_data
```

以下の様にカラム内でデータがネストされています。
![Screen Shot 2022-09-22 at 8.49.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/01499824-3c13-64d7-9998-7fa64370563a.png)

# シンプルな例

基本的な変換処理でコンセプトの基礎を学びましょう。このケースでは、高階関数``transform``が配列``values``に対してイテレーションを行い、関連づけられたラムダ関数をそれぞれの要素に適用し、新たな配列を作成します。ラムダ関数``element + 1``は、それぞれの要素をどの様に操作するのかを指定します。このSQLは以下の様になります

```sql:SQL
SELECT
  key, 
  values,
  TRANSFORM(values, value -> value + 1) AS values_plus_one
FROM
  nested_data
```

配列の各要素が1だけ加算されています。
![Screen Shot 2022-09-22 at 8.51.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/eec6c9a8-5d5c-4725-eafe-3292c112a9f7.png)

変換処理`TRANSFORM(values, value -> value + 1)`は2つのコンポーネントから構成されます:

1. `TRANSFORM(values..)`は高階関数です。これは、入力として配列と匿名関数を受け取ります。内部的には、新規配列のセットアップ、それぞれの要素への匿名関数の適用、出力配列への結果の割り当てを行います。
1. `value -> value + 1`は匿名関数です。この関数は、シンボル`->`で区切られた2つのコンポーネントから構成されます。
    - 引数のリスト。この場合は引数は1つの`value`です。`(x, y) -> x + y`のように、括弧で囲まれたカンマ区切りの引数リストを作成することで複数の引数もサポートしています。
    - 本体。新たな値を計算するために引数と外部の変数を使用できるエクスプレッションです。この場合、`argument`の値に`1`を加算しています。

# 変数のキャプチャ

ラムダ関数では引数だけではなく、他の変数を使用することもできます。これはキャプチャと呼ばれます。トップレベルで定義されている変数や中間のラムダ関数で定義されている変数を使用することができます。例えば、以下の変換処理では`key`(トップレベル)変数を、配列`values`のそれぞれの要素に加算しています。

```sql:SQL
SELECT
  key,
  values,
  TRANSFORM(values, value -> value + key) AS values_plus_key
FROM
  nested_data
```

トップレベルの変数`key`の値を配列の各要素に加算しています。
![Screen Shot 2022-09-22 at 8.53.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/bd7d48a1-d5d5-070c-7c51-127dbadc69b5.png)

# ネスト化

深くネストされたデータを変換したい場合、ネストされたラムダ関数を使用することができます。以下の例では、integerの配列の配列を変換し、ネストされた配列のそれぞれの要素に、`key`(トップレベル)カラムの値と中間配列のサイズを加算しています。

```sql:SQL
SELECT
  key,
  nested_values,
  TRANSFORM(nested_values, values -> TRANSFORM(values, value -> value + key + SIZE(values))) AS new_nested_values
FROM
  nested_data
```
![Screen Shot 2022-09-22 at 8.54.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/8e5b858b-d94f-d924-f7c3-fda457c7f15f.png)

# サポートされる関数

## `transform(array<T>, function<T, U>): array<U>`

入力である`array<T>`のそれぞれの要素に`function<T, U>`を適用することで`array<U>`を変換します。

これは機能的には`map`と同じものとなります。(キーバリューエクスプレッションからmapを作成する)mapエクスプレッションとの混乱を避けるために`transform`と名付けられています。

以下のクエリーでは、それぞれの要素に`key`の値を加算することで配列`values`を変換しています。

```sql:SQL
SELECT   key,
         values,
         TRANSFORM(values, value -> value + key) transformed_values
FROM     nested_data
```
![Screen Shot 2022-09-22 at 8.55.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/50ea1c2c-bb25-7407-1b4b-f00da864e0a4.png)

## `exists(array<T>, function<T, V, Boolean>): Boolean`

入力の`array<T>`の要素が述語`function<T, Boolean>`に合致するかどうかをテストします。

以下の例では、配列`values`に10で割った余りが1になる要素があるかどうかをチェックしています。

```sql:SQL
SELECT
  key,
  values,
  EXISTS(values, value -> value % 10 == 1) filtered_values
FROM
  nested_data
```
![Screen Shot 2022-09-22 at 8.56.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/5edd87d5-1706-c828-83d6-cd8e922519f2.png)

## `filter(array<T>, function<T, Boolean>): array<T>`

入力`array<T>`から述語`function<T, boolean>`にマッチするもののみを追加することで出力`array<T>`にフィルタリングします。

以下の例では、`value > 50`を満たす要素のみからなる`values`配列にフィルタリングしています。

```sql:SQL
SELECT   key,
         values,
         FILTER(values, value -> value > 50) filtered_values
FROM     nested_data
```
![Screen Shot 2022-09-22 at 8.57.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/e1c5ffa6-b73a-3605-065f-5e280938f25b.png)

## `aggregate(array<T>, B, function<B, T, B>, function<B, R>): R`

`function<B, T, B>`を用いて、要素をバッファー`B`にマージし、最終的なバッファーに最後の`function<B, R>`を適用することで、`array<T>`の要素を単一の値`R`にまとめます。`B`の初期値はゼロエクスプレッションによって決定されます。最後の関数はオプションです。最終化の関数を指定しない場合、何も変化させない関数`(id -> id)`が使用されます。

これは、2つのラムダ関数を使う唯一の高階関数です。

以下の例では、`values`配列を単一の(sum)の値に合計(集計)しています。最終化関数(`summed_values`)のバージョンと、最終化関数なしの`summed_values_simple`バージョンを示しています。

**注意** 以下で使用している`REDUCE`関数は`AGGREGATE`関数と同じものです。

```sql:SQL
SELECT   key,
         values,
         REDUCE(values, 0, (value, acc) -> value + acc, acc -> acc) summed_values,
         REDUCE(values, 0, (value, acc) -> value + acc) summed_values_simple
FROM     nested_data
```
![Screen Shot 2022-09-22 at 8.57.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/3dd4c004-9def-c793-fd0c-89307c58c6ab.png)

さらに複雑な集計処理を行うこともできます。以下のコードでは、配列の要素のジオメトリックな平均値を計算しています。

```sql:SQL
SELECT   key,
         values,
         AGGREGATE(values,
           (1 AS product, 0 AS N),
           (buffer, value) -> (value * buffer.product, buffer.N + 1),
           buffer -> Power(buffer.product, 1.0 / buffer.N)) geomean
FROM     nested_data
```
![Screen Shot 2022-09-22 at 8.58.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/0a285fa9-2974-0b0e-cf21-4518c9e3839d.png)


### Databricks 無料トライアル

[Databricks 無料トライアル](https://databricks.com/jp/try-databricks)