元编程

使用元编程设计的程序具有读取、生成、分析或转换其他程序的功能,甚至可以在运行时自行修改代码。

DolphinDB支持生成动态表达式和延迟执行的元编程。通过元编程,用户可以生成SQL语句,并动态执行。

元代码是由”<”和”>”包围的对象或表达式。

相关函数

1. 函数 expr 从对象,运算符或其他元代码生成元代码。

语法: expr(X1, X2, ……) ,其中X可以是对象,运算符或其他元代码。

$ expr(6,<,8);
< 6 < 8 >

$ expr(sum, 1 2 3);
< sum [1,2,3] >

$ a=6;
$ expr(a,+,1);
< 6 + 1 >

$ expr(<a>,+,1);
< a + 1 >

$ expr(<a>,+,<b>);
< a + b >

$ expr(a+7,*,8);
< 13 * 8 >

$ expr(<a+7>,*,8);
< (a + 7) * 8 >

$ expr(not, < a >);
< ! a >

2. 函数 eval 执行给定的元代码。

语法: eval(<X>) ,其中X是被执行的元代码。

$ eval(<1+2>);
3

$ eval(<1+2+3=10>);
0

$ eval(expr(6,<,8));
1

$ eval(expr(sum, 1 2 3));
6

$ a=6; b=9;
$ eval(expr(<a>,+,<b>));
15

3. 函数 sqlCol 将列名转换为表达式。

语法:sqlCol(colNames),其中colNames可以是列名标量或向量。每一个列名都要用反引号、双引号或单引号。

$ sqlCol(`PRC);
< PRC >

$ sqlCol(["PRC", "RET", "DATE", "TICKER"]);
(< PRC >,< RET >,< DATE >,< TICKER >)

4. 函数 sqlColAlias 使用元代码和一个别名(可选参数)来定义一个列。计算列会经常使用此函数。

语法: sqlColAlias(<metacode>, [aliasName])

$ sqlColAlias(<x>, `y);
< x as y >

$ sqlColAlias(<avg(PRC)>, `avgPRC);
< avg(PRC) as avgPRC >

$ sqlColAlias(<avg(PRC)>);
< avg(PRC) as avg_PRC >

5. 函数 sql 可以动态生成SQL语句。

语法: sql(select, from, [where], [groupBy], [groupFlag], [csort], [ascSort], [having], [orderBy], [ascOrder], [limit], [hint])

$ symbol = take(`GE,6) join take(`MSFT,6) join take(`F,6)
$ date=take(take(2017.01.03,2) join take(2017.01.04,4), 18)
$ price=31.82 31.69 31.92 31.8  31.75 31.76 63.12 62.58 63.12 62.77 61.86 62.3 12.46 12.59 13.24 13.41 13.36 13.17
$ volume=2300 3500 3700 2100 1200 4600 1800 3800 6400 4200 2300 6800 4200 5600 8900 2300 6300 9600
$ t1 = table(symbol, date, price, volume);

$ t1;

symbol

date

price

volume

GE

2017.01.03

31.82

2300

GE

2017.01.03

31.69

3500

GE

2017.01.04

31.92

3700

GE

2017.01.04

31.8

2100

GE

2017.01.04

31.75

1200

GE

2017.01.04

31.76

4600

MSFT

2017.01.03

63.12

1800

MSFT

2017.01.03

62.58

3800

MSFT

2017.01.04

63.12

6400

MSFT

2017.01.04

62.77

4200

MSFT

2017.01.04

61.86

2300

MSFT

2017.01.04

62.3

6800

F

2017.01.03

12.46

4200

F

2017.01.03

12.59

5600

F

2017.01.04

13.24

8900

F

2017.01.04

13.41

2300

F

2017.01.04

13.36

6300

F

2017.01.04

13.17

9600

$ x=5000
$ whereConditions = [<symbol=`MSFT>,<volume>x>]
$ havingCondition = <sum(volume)>200>;

$ sql(sqlCol("*"), t1);
< select * from t1 >

$ sql(sqlCol("*"), t1, whereConditions);
< select * from t1 where symbol == "MSFT",volume > x >

$ sql(select=sqlColAlias(<avg(price)>), from=t1, where=whereConditions, groupBy=sqlCol(`date));
< select avg(price) as avg_price from t1 where symbol == "MSFT",volume > x group by date >

$ sql(select=sqlColAlias(<avg(price)>), from=t1, groupBy=[sqlCol(`date),sqlCol(`symbol)]);
< select avg(price) as avg_price from t1 group by date,symbol >

$ sql(select=(sqlCol(`symbol),sqlCol(`date),sqlColAlias(<cumsum(volume)>, `cumVol)), from=t1, groupBy=sqlCol(`date`symbol), groupFlag=0);
< select symbol,date,cumsum(volume) as cumVol from t1 context by date,symbol >

$ sql(select=(sqlCol(`symbol),sqlCol(`date),sqlColAlias(<cumsum(volume)>, `cumVol)), from=t1, where=whereConditions, groupBy=sqlCol(`date), groupFlag=0);
< select symbol,date,cumsum(volume) as cumVol from t1 where symbol == "MSFT",volume > x context by date >

$ sql(select=(sqlCol(`symbol),sqlCol(`date),sqlColAlias(<cumsum(volume)>, `cumVol)), from=t1, where=whereConditions, groupBy=sqlCol(`date), groupFlag=0, csort=sqlCol(`volume), ascSort=0);
< select symbol,date,cumsum(volume) as cumVol from t1 where symbol == "MSFT",volume > x context by date csort volume desc >

$ sql(select=(sqlCol(`symbol),sqlCol(`date),sqlColAlias(<cumsum(volume)>, `cumVol)), from=t1, where=whereConditions, groupBy=sqlCol(`date), groupFlag=0, having=havingCondition);
< select symbol,date,cumsum(volume) as cumVol from t1 where symbol == "MSFT",volume > x context by date having sum(volume) > 200 >

$ sql(select=sqlCol("*"), from=t1, where=whereConditions, orderBy=sqlCol(`date), ascOrder=0);
< select * from t1 where symbol == "MSFT",volume > x order by date desc >

$ sql(select=sqlCol("*"), from=t1, limit=1);
< select top 1 * from t1 >

$ sql(select=sqlCol("*"), from=t1, groupBy=sqlCol(`symbol), groupFlag=0, limit=1);
< select top 1 * from t1 context by symbol >

$ sql(select=(sqlCol(`symbol),sqlCol(`date),sqlColAlias(<cumsum(volume)>, `cumVol)), from=t1, groupBy=sqlCol(`date`symbol), groupFlag=0, hint=HINT_KEEPORDER);
< select [128] symbol,date,cumsum(volume) as cumVol from t1 context by date,symbol >

可以定义一个函数使用 sql 函数来动态生成SQL语句。

$ def f1(t, sym, x){
$    whereConditions=[<symbol=sym>,<volume>x>]
$    return sql(sqlCol("*"),t,whereConditions).eval()
$ };

$ f1(t1, `MSFT, 5000);

symbol

date

price

volume

MSFT

2017.01.04

63.12

6400

MSFT

2017.01.04

62.3

6800

$ f1(t1, `F, 9000);

symbol

date

price

volume

F

2017.01.04

13.17

9600

$ def f2(t, sym, colNames, filterColumn, filterValue){
$    whereConditions=[<symbol=sym>,expr(sqlCol(filterColumn),>,filterValue)]
$    return sql(sqlCol(colNames),t,whereConditions).eval()
$ };

$ f2(t1,`GE, `symbol`date`volume, `volume, 3000);

symbol

date

volume

GE

2017.01.03

3500

GE

2017.01.04

3700

GE

2017.01.04

4600

$ f2(t1,`F, `symbol`date`volume,`price,13.2);

symbol

date

volume

F

2017.01.04

8900

F

2017.01.04

2300

F

2017.01.04

6300

6. 函数 partial 可以创建部分应用。

语法:partial(func, args…)

$ partial(add,1)(2);
3

$ def f(a,b):a pow b
$ g=partial(f, 2)
$ g(3);
8

7. 函数 makeCall 使用指定参数调用函数并生成脚本。与高阶函数 call 的区别是 makeCall 不执行脚本。

在下列例子中,我们创建了 generateReport 函数。它能够以指定格式显示表的某些列。

$ def generateReport(tbl, colNames, colFormat): sql(sqlColAlias(each(makeCall{format},sqlCol(colNames),colFormat),colNames), tbl).eval()
$ t = table(1..100 as id, (1..100 + 2018.01.01) as date, rand(100.0, 100) as price, rand(10000, 100) as qty, rand(`A`B`C`D`E`F`G, 100) as city);
$ t;

id

date

price

qty

city

1

2018.01.02

99.662328

6002

D

2

2018.01.03

24.965461

5836

B

3

2018.01.04

77.006418

5

G

4

2018.01.05

93.930603

5141

G

5

2018.01.06

70.994914

5778

C

6

2018.01.07

11.221769

9403

G

7

2018.01.08

59.387621

4632

G

8

2018.01.09

86.830752

4574

C

9

2018.01.10

53.928317

8397

G

10

2018.01.11

21.123212

6615

B

$ generateReport(t, ["id", "date","price","qty"], ["0", "MM/dd/yyyy", "0.00", "#,###"]);

id

date

price

qty

1

01/02/2018

16.32

9,972

2

01/03/2018

63.10

7,785

3

01/04/2018

30.43

3,629

4

01/05/2018

33.57

2,178

5

01/06/2018

61.12

9,406

6

01/07/2018

43.29

6,955

7

01/08/2018

54.97

9,914

8

01/09/2018

86.20

6,696

9

01/10/2018

90.82

6,141

10

01/11/2018

4.29

3,774

与之等效的SQL脚本是

$ def generateReportSQL(tbl, colNames, colFormat): sql(sqlColAlias(each(makeCall{format},sqlCol(colNames),colFormat),colNames), tbl)
$ generateReportSQL(t, ["id", "date","price","qty"], ["0", "MM/dd/yyyy", "0.00", "#,###"]);
< select format(id, "0") as id,format(date, "MM/dd/yyyy") as date,format(price, "0.00") as price,format(qty, "#,###") as qty from t >

以下3个示例使用了3种不同的方法来创建一个生成SQL语句的函数:

例1:

$ def f1(t, sym, x): select * from t where name=sym, vol>x;
$ f1(t1, `MSFT, 7000);

name

date

PRC

vol

MSFT

2017.01.03

63.12

8800

MSFT

2017.01.03

62.58

7800

我们也可以使用函数 eval 和一个元代码块创建这个函数。

例2:

$ def f2(t, sym, x): eval(<select * from t where name=`MSFT, vol>x >);
$ f2(t1,`MSFT, 7000);

name

date

PRC

vol

MSFT

2017.01.03

63.12

8800

MSFT

2017.01.03

62.58

7800

我们还可以定义一个函数使用 sql 函数来生成SQL语句。当调用该函数时,代码中的sql函数动态地生成SQL语句并执行。对于生成更加复杂的查询,这种方法更加方便灵活。

例3:

$ def f3(t, sym, x){
$ whereConditions=[<name=sym>,<vol>x>]
$ return sql(sqlCol("*"),t,whereConditions).eval()
$ };

$ f3(t1, `MSFT, 7000);

name

date

PRC

vol

MSFT

2017.01.03

63.12

8800

MSFT

2017.01.03

62.58

7800

$ f3(t1, `F, 9000);

name

date

PRC

vol

F

2017.01.04

13.17

在这3种方法中,函数 sql 最灵活。它可以动态地生成SQL语句。

$ def f4(t, sym, colNames, filterColumn, filterValue){
$ whereConditions=[<name=sym>,expr(sqlCol(filterColumn),>,filterValue)]
$ return sql(sqlCol(colNames),t,whereConditions).eval()
$ };
$ f4(t1,`MSFT, `name`date`vol, `vol, 7000);

name

date

vol

MSFT

2017.01.03

8800

MSFT

2017.01.03

7800

$ f4(t1,`F, `name`date`vol,`PRC,13.2);

name

date

vol

F

2017.01.04

8900

F

2017.01.04

2300

F

2017.01.04

6300

8. 函数 binaryExpr 可以生成一个二元运算的元代码。

语法:binaryExpr(X, Y, optr)

$ t = table(reshape(rand(1.0, 200), 20:10));
$ n=10;
$ weights = array(ANY, n).fill!(0..(n-1), 1..n)\n;
$ names = t.colNames();
$ tp = binaryExpr(sqlCol(names), weights, *);
$ tp;
(< col0 * 0.1 >,< col1 * 0.2 >,< col2 * 0.3 >,< col3 * 0.4 >,< col4 * 0.5 >,< col5 * 0.6 >,< col6 * 0.7 >,< col7 * 0.8 >,< col8 * 0.9 >,< col9 * 1 >)

9. 函数 unifiedExpr 可以生成一个多元运算表达式的元代码。

语法: unifiedExpr(objs, optrs)

以下例子在binaryExpr例子的基础上,生成一个sql的select语句代码, 用于对每列的值加权求和:

$ selects = sqlColAlias(unifiedExpr(binaryExpr(sqlCol(names), weights, *), take(+, n-1)), "weightedSum");
$ re = sql(selects, t);
$ re;
< select (col0 * 0.1) + (col1 * 0.2) + (col2 * 0.3) + (col3 * 0.4) + (col4 * 0.5) + (col5 * 0.6) + (col6 * 0.7) + (col7 * 0.8) + (col8 * 0.9) + (col9 * 1) as weightedSum from tc0ccbd6a00000000 >
$ re.eval()

weightedSum

2.1239

3.5725

3.5009

3.3487

2.8268

1.9753

2.4287

2.9222

3.0469

2.9751

2.8848

2.993

2.7185

2.2578

2.6231

2.3871

3.0311

2.0183

2.897

2.5982

10. 函数 makeUnifiedCall 可以生成一个函数调用的元代码。与高阶函数 unifiedCallmakeUnifiedCall 的区别是,makeUnifiedCall 不执行脚本。

语法: makeUnifiedCall(func, args)

以下例子生成一个和9中例子等价的元代码:

$ selects = sqlColAlias(makeCall(flatten, makeCall(dot, makeUnifiedCall(matrix, sqlCol(names)), 1..n\n)), "weightedSum");
$ re = sql(selects, t);
$ re;
< select flatten(dot(matrix(col0, col1, col2, col3, col4, col5, col6, col7, col8, col9), [0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9,1])) as weightedSum from tc0ccbd6a00000000 >
$ re.eval();

weightedSum

2.1239

3.5725

3.5009

3.3487

2.8268

1.9753

2.4287

2.9222

3.0469

2.9751

2.8848

2.993

2.7185

2.2578

2.6231

2.3871

3.0311

2.0183

2.897

2.5982

使用元编程生成报表

我们可以使用函数 format 函数生成报表。format函数有两个参数:列名和对应的格式字符串。格式字符串与Java中的数字和日期格式字符串相似。我们使用6种符号(0/#/,/./%/E)来表示数字的格式,使用9种符号(y/M/d/H/h/m/s/S/n)表示时序数据的格式。例如:

输入数据

格式字符串

输出数据

2012.12.05T15:30:59.125

yyyy-MM-dd HH:mm

2012-12-05 15:30

2012.12.05

MMMddyyyy

DEC052012

0.1356

0.0%

13.6%

1234567.42

#,###.0

1,234,567.4

1234567.42

0.00####E0

1.234567E6

下面的例子创建了一个自定义函数,给定表、列名和每个列的格式字符串,就能生成报表。sqlColsqlColAliassqlmakeCall 函数可以用于生成SQL语句,调用 eval 函数来生成报表,makeCall 函数调用脚本并生成一系列元代码,makeCall 函数的第一个参数是函数,其他参数是函数所需的参数。

$ def generateReport(tbl, colNames, colFormat){
$    colCount = colNames.size()
$    colDefs = array(ANY, colCount)
$    for(i in 0:colCount){
$       if(colFormat[i] == "")
$          colDefs[i] = sqlCol(colNames[i])
$       else
$          colDefs[i] = sqlCol(colNames[i], format{,colFormat[i]})
$    }
$    return sql(colDefs, tbl).eval()
$ }

生成100行4列(列名为id, date, price和qty)的样本表,随后调用generateReport函数生成报表。

$ t = table(1..100 as id, (1..100 + 2018.01.01) as date, rand(100.0, 100) as price, rand(10000, 100) as qty);
$ t;

id

date

price

qty

1

2018.01.02

61.483376

8733

2

2018.01.03

21.254616

8365

3

2018.01.04

37.408301

444

4

2018.01.05

70.608384

9944

5

2018.01.06

80.361811

7185

$ generateReport(t, ["id", "date","price","qty"], ["0", "MM/dd/yyyy", "0.00", "#,###"]);

id

date

price

qty

1

01/02/2018

61.48

8,733

2

01/03/2018

21.25

8,365

3

01/04/2018

37.41

444

4

01/05/2018

70.61

9,944

5

01/06/2018

80.36

7,185

也可使用元编程生成过滤条件。

$ def generateReportWithFilter(tbl, colNames, colFormat, filter){
$    colCount = colNames.size()
$    colDefs = array(ANY, colCount)
$    for(i in 0:colCount){
$       if(colFormat[i] == "")
$          colDefs[i] = sqlCol(colNames[i])
$       else
$          colDefs[i] = sqlCol(colNames[i], format{,colFormat[i]})
$    }
$    return sql(colDefs, tbl, filter).eval()
$ }
$ generateReportWithFilter(t, ["id","date","price","qty"], ["","MM/dd/yyyy", "00.00", "#,###"], < id>10 and price<20 >);

id

date

price

qty

11

01/12/2018

11.21

7,033

18

01/19/2018

09.97

6,136

29

01/30/2018

06.33

3,834

31

02/01/2018

05.52

6,851

38

02/08/2018

14.55

7,482