关于lisp:多重返回值的阵营九宫格

50次阅读

共计 3920 个字符,预计需要花费 10 分钟才能阅读完成。

通常在糊业务代码的时候,不论是函数、办法,还是宏,都只会有一个返回值。比方在 C 语言用于查看一个字符是否为阿拉伯数字的 isdigit 函数就只会返回是(1)或否(0)

#include <ctype.h>
#include <stdio.h>

int
main(int argc, char *argv[])
{
    char c = 'a';
    printf("isdigit('%c') is %d\n", c, isdigit(c));
    return 0;
}

但有时候如果一个函数、办法,或宏能够返回多个值的话会更加不便。例如,在 Python 中 dict 类型有一个实例办法 get,它能够获得dict 实例中与给定的键对应的值。但如果有一个键在字典中的值为 None,那么光凭get 的返回值无奈精确判断这个键是否存在——除非你给它一个非 None 的默认值

# -*- coding: utf8 -*-
def test(d, key):
    print("d.get('{0}') is {1}\t'{0}'in d is {2}".format(key, d.get(key), key in d))

if __name__ == '__main__':
    d = {
        'foo': 'bar',
        'baz': None,
    }
    test(d, 'foo')
    test(d, 'baz')

倒退了这么多年的编程语言,又怎么会连一次调用、多值返回这么简略的事件都做不到呢。事实上,有各种各样、各显神通的返回多个值的办法,我给其中的一些做了个分类

Lisp 的multiple-value-bind

Common Lisp(简称为 CL)的多重返回值当之无愧是其中最正统、最好用的实现形式。以它的内置函数 truncate 为例,它的第一个返回值为第一个参数除以第二个参数的商,第二个返回值为对应的余数

CL-USER> (truncate 10 3)
3
1

如果不加润饰地调用truncate,就像其它只返回一个值的函数一样,也只会拿到一个返回值

CL-USER> (let ((q (truncate 10 3)))
           (format t "q = ~D~%" q))
q = 3

除非用 multiple-value-bind 来捕捉一个函数产生的所有返回值

CL-USER> (multiple-value-bind (q r)
             (truncate 10 3)
           (format t "q = ~D~8Tr = ~D~%" q r))
q = 3   r = 1

CL 的计划的长处在于它非常灵便。即便将一个函数从返回单个值改为返回多个值,也不会导致本来调用该函数的地位要全副批改一遍——对批改关闭,对扩大凋谢(误)。

Go 的多重返回值

踩在 C 语言肩膀上的 Go 也可能从函数中返回多个值。在 io/ioutil 包的官网文档中有大量的例子,比方用 ReadAll 办法从字符串衍生的流中读取全部内容,就会返回两个值

package main

import (
    "fmt"
    "io/ioutil"
    "log"
    "strings"
)

func main() {
    s := "Hello, world!"
    reader := strings.NewReader(s)
    bytes, err := ioutil.ReadAll(reader)
    if err != nil {log.Fatal(err)
    }
    fmt.Printf("bytes is %s", bytes)
}

Go 以这种形式取代了 C 语言中用返回值表白胜利与否、再通过指针传出读到的数据的格调。因为这个模式在有用的 Go 程序中到处呈现,因而 Gopher 们用的都是定制的键盘(误)

不同于前文的multiple-value-bind,如果一个函数或办法返回多个值,那么调用者必须捕捉每一个值,否则编译无奈通过

➜  try cat try_read_all_ignore_err.go
package main

import (
    "fmt"
    "io/ioutil"
    "strings"
)

func main() {
    s := "Hello, world!"
    reader := strings.NewReader(s)
    bytes := ioutil.ReadAll(reader)
    fmt.Printf("bytes is %s", bytes)
}
➜  try go build try_read_all_ignore_err.go
# command-line-arguments
./try_read_all_ignore_err.go:12:8: assignment mismatch: 1 variable but ioutil.ReadAll returns 2 values

这一要求也是正当的,毕竟多重返回值机制次要用于向调用者传递出错起因——既然可能出错,那么就必须要查看一番。

Python 和 Rust 的解构

就像 CL 的 truncate 函数一样,Python 中的函数 divmod 也能够同时返回两个数相除的商和余数,并且咋看之下也是返回多个值的模式

# -*- coding: utf8 -*-
if __name__ == '__main__':
    q, r = divmod(10, 3)
    print('q = {}\tr = {}'.format(q, r))

但实质上,这是因为 Python 反对解构,同时 divmod 返回的是一个由商和余数组成的元组。这样的做法与 CL 的真·奥义·多重返回值的差别在于,如果只想要 divmod 的第一个值,那么等号左侧也要写成对应的构造

# -*- coding: utf8 -*-
if __name__ == '__main__':
    q, _ = divmod(10, 3)
    print('q = {}'.format(q))

在反对解构的语言中都能够模拟出多重返回值,例如 Rust

fn divmod(a: u32, b: u32) -> (u32, u32) {(a / b, a % b)
}

fn main() {let (q, r) = divmod(10, 3);
    println!("q = {}\tr = {}", q, r);
}

Prolog 的归一

到了 Prolog 这里,画风就有点不一样了。首先 Prolog 既没有函数,也没有办法,更没有宏。在 Prolog 中,像 length/2member/2这样的货色叫做 functor,它们之于 Prolog 中的列表,就犹如 CL 的lengthmember之于列表、Python 的 len 函数和 in 操作符之于列表,JavaScript 的 length 属性和 indexOf 办法之于数组……

其次,Prolog 并不“返回”一个 functor 的“调用后果”,它只是判断输出的查问是否成立,以及给出使查问成立的变量值。在第一个查问中,length/2的第二个参数为变量 L,因而 Prolog 给出了使这个查问成立的L 的值 4;第二个查问中没有变量,Prolog 只是简略地给出查问是否成立;第三个查问中,Prolog 给出了四个可能使查问成立的变量 X 的值。

因为 Prolog 会给出查问中每一个变量的值,能够用这个个性来模仿多重返回值。例如,能够让 Prolog 一次性给出两个数字的和、差、积,和商

麻烦之处在于就算只想要失去两数之和,也必须用占位符填在后三个参数上:jjcc(10, 3, S, _, _, _)

舞弊的指针与全局变量

只管在开篇的时候提到了 C 语言中的函数无奈返回多个值,但如果像上文的 Prolog 那般容许批改参数的话,C 语言也是能够做到的,谁让它有指针这个强力个性呢。例如,stat(2)函数就会将对于一个文件的信息填充到参数中所指向的构造体的内存中

#include <stdio.h>
#include <sys/stat.h>

int
main(int argc, char *argv[])
{
    char *path = "./try_stat.c";
    struct stat buf;
    stat(path, &buf);
    printf("inode's number of %s is %llu\n", path, buf.st_ino);
    return 0;
}

查看 man 2 stat 能够晓得 struct stat 类型中有十分多的内容,这显然也是一种多重返回值。同样的手法,在 Go 中也能够使用,例如用于把从数据库中读取进去的行的数据写入指标数据结构的 Scan 办法。

最初,如果只有能让调用者感知就行,那么全局变量未尝不是一种通用的多重返回值机制。例如在 C 语言中的 strtol 函数,就会在无奈转换出任何数字的时候返回 0 并设置 errno,因而查看errno 是必须的步骤

#include <stdio.h>
#include <stdlib.h>
#include <sys/errno.h>

void try_conversion(const char *str)
{long num = strtol(str, NULL, 10);
    if (errno == EINVAL || errno == ERANGE)
    {char message[64];
        snprintf(message, sizeof(message), "strtol(\"%s\")", str);
        perror(message);
        return;
    }
    printf("strtol(\"%s\") is %ld\n", str, num);
}

int
main(int argc, char *argv[])
{try_conversion("233");
    try_conversion("0");
    try_conversion("lisp");
    return 0;
}

鉴于 errno 是一个全局变量,strtol的使用者齐全有可能遗记要查看。相比之下,Go 的 strconv 包的函数都将转换过程中的谬误以第二个参数的模式返回给调用者,用起来更平安。

后记

依照《代码写得不好,不要总感觉是本人形象得不好》这篇文章的说法,代码写成什么样子齐全是由产品经理决定的。但产品经理又怎么会在意你用的技术是怎么实现多重返回值的呢。综上所述,这个个性没用(误)。

全文完。

正文完
 0