Racket FFI调用GSL库
Racket 是什么?
Racket是Lisp编程语言家族的一员,它与SBCL所实现的Common Lisp不同,所有符号都定义在同一个命名空间中,这个方式称为Lisp-1,而SBCL的方式称为Lisp-2。
Racket的设计使得它更适合于教学和快速开发,并且它采用了更有一致性的语法。所有的函数和变量都采用define来定义。可以看看Racket的Cheat Sheet,就会发现Racket的语法非常简洁和一致,几乎所有的定义都是通过define来完成的,无论是函数、变量、模块还是类等等。相比之下,SBCL的Common Lisp有更多的定义形式,比如defun、defvar、defmacro、defclass等等,这些定义形式虽然功能强大,但也增加了学习的复杂度。

Racket的生态系统可能不见得有SBCL丰富(见仁见智),但是Racket的文档和社区可比Common Lisp好太多了。我整天看Lisp的文档简直像看天书,大佬们都说,看源代码就行了……好吧,你写的你说啥就是啥……
Racket的文档,有统一的风格,清晰的结构,大概率有较为详细的说明。而且Racket的社区也确实比common lisp更活跃,尤其是在教学和快速开发方面。
官方安装的Racket自带一个包管理系统,叫做raco,可以用来安装各种库和工具。Racket的包管理系统相当于Python的pip或者Node.js的npm,使用起来非常方便。而且,虽然丑丑的,Racke他还带了一个叫做DrRacket的IDE,虽然不太好用比较简陋,但对于初学者来说还是挺友好的。
当然用raco可以管理racket的包,当然我们也可以直接用racket的ffi接口来调用C语言的库,这样就可以利用C语言库的性能和功能了。GSL(GNU Scientific Library)就是一个非常强大的科学计算库,提供了各种数学函数、数值方法和算法,可以用来进行数值分析、线性代数、微分方程求解等等。通过Racket的FFI接口,我们可以调用GSL库中的函数来进行科学计算,这样就可以在Racket中愉快算数字。
下面是一个一个简单的例子,展示了如何在Racket中调用GSL库来求解多项式的复数根。
GSL中的多项式求根
下面是官方文档中求取$x^5 - 1 = 0$的根的例子。例子要求解$x^5 - 1 = 0$的根,也就是求解$x^5 - 1 = 0$的复数根。显然,方程有五个根,分别是1(实数根)和四个复数根。
$$ 1, e^{2\pi i/5}, e^{4\pi i/5}, e^{6\pi i/5}, e^{8\pi i/5} $$这个多项式的系数可以用六个浮点数的数组表达,分别是,-1(常数项)和1(最高次项)和中间的四个0。数组按照从常数项到最高次项的顺序排列。调用GSL库中的函数来求解这个多项式的复数根。从下面的C语言代码可以看到,求解的复数根存在一个长度为10的数组中,前两个元素是第一个根的实部和虚部,接下来的两个元素是第二个根的实部和虚部,以此类推。最后打印出每个根的实部和虚部。
1#include <stdio.h>
2#include <gsl/gsl_poly.h>
3
4int
5main (void)
6{
7 int i;
8 /* coefficients of P(x) = -1 + x^5 */
9 double a[6] = { -1, 0, 0, 0, 0, 1 };
10 double z[10];
11
12 gsl_poly_complex_workspace * w
13 = gsl_poly_complex_workspace_alloc (6);
14
15 gsl_poly_complex_solve (a, 6, w, z);
16
17 gsl_poly_complex_workspace_free (w);
18
19 for (i = 0; i < 5; i++)
20 {
21 printf ("z%d = %+.18f %+.18f\n",
22 i, z[2*i], z[2*i+1]);
23 }
24
25 return 0;
26}
Racket FFI调用GSL库
1#lang racket
下面就是Racket调用GSL库的代码了,首先是一些基础设施的定义,包括加载动态库和定义抽象的FFI对象的宏。这个宏调用了Racket的get-ffi-obj函数来从动态库中获取函数(指针),这个函数需要输入C语言函数的名称、库对象和函数类型。我们在这个宏中间,用define来把这个对象绑定到一个Racket变量上,这样我们就可以在后续的代码中直接使用这个变量来调用GSL函数了。
1(require ffi/unsafe)
2(require ffi/vector)
3
4(define gsl-lib
5 (let ([lib "gsl.dll"])
6 (ffi-lib lib)))
7
8;; Abstract FFI object definition
9(define-syntax-rule (define-ffi-obj racket-name c-name type)
10 (define racket-name (get-ffi-obj c-name gsl-lib type)))
下面就利用上述宏来导入GSL库中求解多项式根的相关函数了。我们定义了三个FFI对象:poly-complex-workspace-alloc用于分配工作空间,poly-complex-workspace-free用于释放工作空间,poly-complex-solve用于求解多项式的复数根。从这里可以看到Racket的ffi接口中定义了对应于C语言类型的Racket符号,比如_fun表示函数类型,_ulong表示无符号长整数(通常size_t就是这个类型),_pointer表示指针类型,_void表示无返回值的函数,_int表示整数类型等等。
1;; Minimal FFI bindings for GSL polynomial solver using idiomatic Racket names
2(define-ffi-obj poly-complex-workspace-alloc
3 "gsl_poly_complex_workspace_alloc"
4 (_fun _ulong -> _pointer))
5
6(define-ffi-obj poly-complex-workspace-free
7 "gsl_poly_complex_workspace_free"
8 (_fun _pointer -> _void))
9
10(define-ffi-obj poly-complex-solve
11 "gsl_poly_complex_solve"
12 (_fun _pointer _ulong _pointer _pointer -> _int))
1;; Format a complex number as a string with specified precision (12 decimal places by default)
2(define (format-complex re im [precision 12])
3 (string-append
4 (if (negative? re)
5 "-"
6 " ")
7 (real->decimal-string (abs re) precision)
8 (if (negative? im)
9 " - "
10 " + ")
11 (real->decimal-string (abs im) precision)
12 "i"))
其实看到上面这个函数,感觉Racket或者Lisp的语言最好的一点就是,修改的时候,非常自然,直接插入一个括号对在函数的参数中,完全没有心理负担。
在racket中,可以用let来构造局部作用域的变量绑定;但是在这里也可以直接使用define来定义变量。这里define的作用域是整个函数体,所以在函数体中都可以访问这些变量,有时候,感觉这样比let要思维负担小一点。也可能这只是我的大脑还不够Lisp化吧……
我们定义了一个函数solve-poly-demo,它接受一个参数n,表示多项式的阶数,当然采用[]这个Lisp异端来表示可选参数也是挺清晰的,其默认值为7。这个函数的作用是求解多项式$P(x) = -1 + x^n$的复数根,并打印出来。
1;; Minimal usage: solve P(x) = -1 + x^n
2(define (solve-poly-demo [n 7])
3 ;; We want to solve x^n - 1 = 0, which has n roots (the nth roots of unity)
4 (define poly-size (+ 1 n)) ; degree n polynomial has n+1 coefficients
5 (define a (make-f64vector poly-size 0.0))
6 (f64vector-set! a 0 -1.0)
7 (f64vector-set! a n 1.0)
8 (define z (make-f64vector (* 2 n) 0.0))
9 (define w (poly-complex-workspace-alloc poly-size))
10 (poly-complex-solve (f64vector->cpointer a) poly-size w (f64vector->cpointer z))
11 (poly-complex-workspace-free w)
12 (printf "x^~a - 1 = 0\n" n)
13 (for ([i (in-range n)])
14 (printf " z~a = ~a\n"
15 i
16 (format-complex (f64vector-ref z (* 2 i))
17 (f64vector-ref z (+ 1 (* 2 i)))))))
在这之后,就可以调用solve-poly-demo函数来求解不同阶数的多项式解:
1;; Run the demo
2(for ([n (in-list '(3 4 5 6 7 8 9 10))])
3 (solve-poly-demo n))
或者,采用更加Racket式的方式:
1;; Run the demo
2(for-each solve-poly-demo '(3 4 5 6 7 8 9 10))
当然,这个循环跟传统的map非常类似,一般而言就是map是为了得到一个新的列表,而for-each是为了执行副作用(比如打印输出),所以在这里用for-each更合适一些。
当然,要运行这个程序,还需要把一个gsl.dll文件放在当前目录或者任何系统可以找到的路径下,这个DLL文件就是GSL库的动态链接库,里面包含了我们需要调用的函数的实现。这个DLL文件可以从GSL的官方网站上下载,或者自己编译GSL库来生成这个DLL文件。
推荐下载方式:nuget gsl-msvc-x64。这个包提供了预编译的GSL库的DLL文件,适用于Windows系统,并且是针对x64架构编译的。下载这个包之后,可以从其中提取出gsl.dll文件。
Linux类似的系统就简单了,直接安装GSL库就行了,安装完成之后,系统会把GSL库的动态链接库放在一个标准的位置,Racket的FFI接口就可以找到并加载这个库了。通常情况下,在Linux系统上安装GSL库之后,Racket的FFI接口就可以直接使用这些库,当然gsl.dll是Windows特有的,Linux上是libgsl.so之类。
文章标签
|-->lisp |-->编程 |-->racket |-->ffi |-->gsl |-->科学计算
- 本站总访问量:loading次
- 本站总访客数:loading人
- 可通过邮件联系作者:Email大福
- 也可以访问技术博客:大福是小强
- 也可以在知乎搞抽象:知乎-大福
- Comments, requests, and/or opinions go to: Github Repository