[翻译]为什么静态语言会受到复杂性的影响?

翻译自 Why Static Languages Suffer From Complexity?

前言

在这里插入图片描述
编程语言设计界的人们努力使他们的语言更具表现力,具有更强大的类型系统,主要是通过避免最终软件中的代码重复来增加代码开发的效率,然而,他们的语言越有表现力,重复就会越突然地渗透到语言本身。
这就是我所说的静态-动态的二态性(biformity):每当你在你的语言中引入一个新的语言抽象时,它可能驻留在静态级别,动态级别,或者两个级别都有。在前两种情况下,抽象仅位于一个特定级别,您将引入语言的不一致性(inconsistency);在后一种情况下,您不可避免地会引入特性上的二态性(feature biformity)
正如我们所知,静态级别是指编译期执行的语句块。同样,动态级别是在运行时执行的语句块。因此,典型的控制流运算符(如if/while/for/return,数据结构以及过程procedure)是动态的,而静态类型系统特性(type system features)和语法宏(syntactical macros)是静态的。从本质上讲,大多数静态语言抽象在动态空间中都有它们的对应关系,反之亦然:
在这里插入图片描述
在以下各节中,在进一步阐述问题之前,让我向您展示如何使用静态和动态方法实现逻辑等效的程序。大多数示例都是用 Rust 编写的,但可以应用于具有足够表达能力的类型系统的任何其他通用编程语言;请记住,本文与语言无关,并且关注一般的PLT哲学,而不是特定的编程语言实现。如果您觉得内容过多,可以直接跳到相关章节。

Record type - Array

考虑日常使用record types的场景

struct Automobile {
    wheels: u8,
    seats: u8,
    manufacturer: String,
}

fn main() {
    let my_car = Automobile {
        wheels: 4,
        seats: 4,
        manufacturer: String::from("X"),
    };

    println!(
        "My car has {} wheels and {} seats, and it was made by {}.",
        my_car.wheels, my_car.seats, my_car.manufacturer
    );
}

(这里Automobile大小在编译期可以确定,所以是static record-type–译者注)
可以用arrays做相同的实现:

use std::any::Any;

#[repr(usize)]
enum MyCar {
    Wheels,
    Seats,
    Manufacturer,
}

fn main() {
    let my_car: [Box<dyn Any>; 3] = [Box::new(4), Box::new(4), Box::new("X")];

    println!(
        "My car has {} wheels and {} seats, and it was made by {}.",
        my_car[MyCar::Wheels as usize]
            .downcast_ref::<i32>()
            .unwrap(),
        my_car[MyCar::Seats as usize].downcast_ref::<i32>().unwrap(),
        my_car[MyCar::Manufacturer as usize]
            .downcast_ref::<&'static str>()
            .unwrap()
    );
}

如果我们指定了不正确的类型做.downcast_ref,我们将遇到panic。但是程序的逻辑保持不变,只是我们将类型检查提升到运行时。

更进一步,我们可以将静态类型Automobile编码为异构列表heterogenous list

use frunk::{hlist, HList};

struct Wheels(u8);
struct Seats(u8);
struct Manufacturer(String);
type Automobile = HList![Wheels, Seats, Manufacturer];

fn main() {
    let my_car: Automobile = hlist![Wheels(4), Seats(4), Manufacturer(String::from("X"))];

    println!(
        "My car has {} wheels and {} seats, and it was made by {}.",
        my_car.get::<Wheels, _>().0,
        my_car.get::<Seats, _>().0,
        my_car.get::<Manufacturer, _>().0
    );
}

此版本强制执行与automobile-static.rs(上上个代码)完全相同的类型检查,但还提供了与普通集合一样操作Automobile的方法!例如,我们可能想要反转我们的汽车:

assert_eq!(
    my_car.into_reverse(),
    hlist![Manufacturer(String::from("X")), Seats(4), Wheels(4)]
);

或者我们可能想用别人的车拉我们的车:

let their_car = hlist![Wheels(6), Seats(4), Manufacturer(String::from("Y"))];
assert_eq!(
    my_car.zip(their_car),
    hlist![
        (Wheels(4), Wheels(6)),
        (Seats(4), Seats(4)),
        (Manufacturer(String::from("X")), Manufacturer(String::from("Y")))
    ]
);

…等等
但是,有时我们可能希望将类型计算type-level computation(指的是类型系统的类型等价,类型相容,类型推理以及类型自带计算–译者注)应用于普通的struct s 和enum s,但我们无法做到这一点,因为我们无法从相应的类型名称中提取类型定义的结构(字段fields和类型/变量types/variants及其函数签名function signatures),如果此类型位于我们的crate外部,我们无法为其提供派生的宏。为了解决这个问题,Frunk开发人员决定创建这样一个过程宏procedural macro,通过实现泛型Generic来检查类型定义的内部结构;它具有type Repr用来关联类型,该类型在实现时等于某种形式的可操作HList。尽管如此,由于 Rust 的上述限制,所有没有此派生宏的其他类型的类型(嗯,透明类型,如 DTOs)都是不可扫描的。
你玩过Rust吗?赶紧上Stream下载吧!

Sum type - Tree

人们可能会发现求和类型非常适合表示 AST 节点:

use std::ops::Deref;

enum Expr {
    Const(i32),
    Add(Box<Expr>, Box<Expr>),
    Sub(Box<Expr>, Box<Expr>),
    Mul(Box<Expr>, Box<Expr>),
    Div(Box<Expr>, Box<Expr>),
}

use Expr::*;

fn eval(expr: &Box<Expr>) -> i32 {
    match expr.deref() {
        Const(x) => *x,
        Add(lhs, rhs) => eval(&lhs) + eval(&rhs),
        Sub(lhs, rhs) => eval(&lhs) - eval(&rhs),
        Mul(lhs, rhs) => eval(&lhs) * eval(&rhs),
        Div(lhs, rhs) => eval(&lhs) / eval(&rhs),
    }
}

fn main() {
    let expr: Expr = Add(
        Const(53).into(),
        Sub(
            Div(Const(155).into(), Const(5).into()).into(),
            Const(113).into(),
        )
        .into(),
    );

    println!("{}", eval(&expr.into()));
}

使用tagged trees也可以完成同样的操作:

use std::any::Any;

struct Tree {
    tag: i32,
    value: Box<dyn Any>,
    nodes: Vec<Box<Tree>>,
}

const AST_TAG_CONST: i32 = 0;
const AST_TAG_ADD: i32 = 1;
const AST_TAG_SUB: i32 = 2;
const AST_TAG_MUL: i32 = 3;
const AST_TAG_DIV: i32 = 4;

fn eval(expr: &Tree) -> i32 {
    let lhs = expr.nodes.get(0);
    let rhs = expr.nodes.get(1);

    match expr.tag {
        AST_TAG_CONST => *expr.value.downcast_ref::<i32>().unwrap(),
        AST_TAG_ADD => eval(&lhs.unwrap()) + eval(&rhs.unwrap()),
        AST_TAG_SUB => eval(&lhs.unwrap()) - eval(&rhs.unwrap()),
        AST_TAG_MUL => eval(&lhs.unwrap()) * eval(&rhs.unwrap()),
        AST_TAG_DIV => eval(&lhs.unwrap()) / eval(&rhs.unwrap()),
        _ => panic!("Out of range"),
    }
}

fn main() {
    let expr = /* Construction omitted... */;

    println!("{}", eval(&expr));
}

与我们对struct Automobile的操作类似,我们可以使用frunk::corproduct表示

Value - Associated type

我们可能希望使用标准运算符!否定布尔值

fn main() {
    assert_eq!(!true, false);
    assert_eq!(!false, true);
}

通过associated types也可以完成同样的操作

use std::marker::PhantomData;

trait Bool {
    type Value;
}

struct True;
struct False;

impl Bool for True { type Value = True; }
impl Bool for False { type Value = False; }

struct Negate<Cond>(PhantomData<Cond>);

impl Bool for Negate<True> {
    type Value = False;
}

impl Bool for Negate<False> {
    type Value = True;
}

const ThisIsFalse: <Negate<True> as Bool>::Value = False;
const ThisIsTrue: <Negate<False> as Bool>::Value = True;

事实上,Rust 类型系统的图灵完备性是建立在这种原理与类型归纳相结合的基础上的(我们将在稍后看到)。每次你看到一个普通的Rust值时,要知道它在计算意义上的类型上有其形式上的对应关系。每次你编写一些算法时,它使用概念上等效的结构在类型系统上有其对应关系。如果你对如何感兴趣,上面的文章提供了一个数学证明:首先,作者使用dynamic特性实现了Smallfuck:一个sum type,模式匹配,递归,然后使用statics特性:logic on traitsassociated types等。

Recursion-Type-level induction

让我再给你们看一个例子,这一次请集中精神!

use std::ops::Deref;

#[derive(Clone, Debug, PartialEq)]
enum Nat {
    Z,
    S(Box<Nat>),
}

fn add(lhs: &Box<Nat>, rhs: &Box<Nat>) -> Nat {
    match lhs.deref() {
        Nat::Z => rhs.deref().clone(), // I
        Nat::S(next) => Nat::S(Box::new(add(next, rhs))), // II
    }
}

fn main() {
    let one = Nat::S(Nat::Z.into());
    let two = Nat::S(one.clone().into());
    let three = Nat::S(two.clone().into());

    assert_eq!(add(&one.into(), &two.into()), three);
}

这是自然数的皮亚诺编码。在add函数中,我们使用递归来计算总和,使用模式匹配以找出停止的位置。
由于递归对应于类型归纳,而模式匹配对应于多个实现,因此在编译时(playground)也可以执行相同的操作:

use std::marker::PhantomData;

struct Z;
struct S<Next>(PhantomData<Next>);

trait Add<Rhs> {
    type Result;
}

// I
impl<Rhs> Add<Rhs> for Z {
    type Result = Rhs;
}


// II
impl<Lhs: Add<Rhs>, Rhs> Add<Rhs> for S<Lhs> {
    type Result = S<<Lhs as Add<Rhs>>::Result>;
}

type One = S<Z>;
type Two = S<One>;
type Three = S<Two>;

const THREE: <One as Add<Two>>::Result = S(PhantomData);

推导过程(译者注):
Add<Two> -> Result = S<One>
<S<Z> as Add<S<S<Z>>>
Lhs S<Z>
Rhs S<Z>
Three : <S<Z>> as <<S<Z> as S<Z>>::Result
这里,impl ... for Z是基本情况(终止情况),impl ... for S<Lhs>是归纳步骤(递归情况) - 类似于我们使用的模式匹配。同样,如第一个示例所示,归纳的工作原理是将第一个参数简化为Z<Lhs as Add<Rhs>>::Result :就像add(next, rhs) - 它再次调用模式匹配以进一步推动计算。请注意,这两个特性实现确实属于相同的逻辑函数实现;它们看起来是分离的,因为我们对type-level numberZS<Next> ) 执行模式匹配。这有点类似于我们在Haskell中看到的情况,其中每个模式匹配情况看起来像一个单独的函数定义:

import Control.Exception

data Nat = Z | S Nat deriving Eq

add :: Nat -> Nat -> Nat
add Z rhs = rhs -- I
add (S next) rhs = S(add next rhs) -- II

one = S Z
two = S one
three = S two

main :: IO ()
main = assert ((add one two) == three) $ pure ()

Type-level logic reified

请添加图片描述
本文的目的只是为了传达statics-dynamics biformity背后的直觉,而不是提供正式的证明 - 对于后者,请参阅一个名为type-operator的令人敬畏的库(由在类型上实现Smallfuck的同一个人)。从本质上讲,它是一种算法宏eDSL,可以归结为具有traits的type-level操作:您可以定义代数数据类型并对其执行数据操作,类似于在 Rust 中通常执行的方式,但最终,整个代码将停留在type-level上。有关更多详细信息,请参阅翻译规则和同一作者的优秀指南。另一个值得注意的项目是Fortraith,它是一个"编译时编译器,将Forth编译为编译时特征表达式":

forth!(
    : factorial (n -- n) 1 swap fact0 ;
    : fact0 (n n -- n) dup 1 = if drop else dup rot * swap pred fact0 then ;
    5 factorial .
);

上面的代码将简单的阶乘实现转换为对特征和相关类型的计算。稍后,您将获得如下结果:

println!(
    "{}",
    <<<Empty as five>::Result as factorial>::Result as top>::Result::eval()
);

在考虑了上述所有内容之后,很明显,无论你如何称呼它,逻辑部分都保持不变:无论是静态还是动态。

The unfortunate consequenes of being static

Are you quite sure that all those bells and whistles, all those wonderful facilities of your so called powerful programming languages, belong to the solution set rather than the problem set?
你是否非常确定所有这些花里胡哨的东西,所有这些你所谓的强大编程语言的精彩设施,都属于解决方案集而不是问题集?
Edsger Dijkstra (Edsger Dijkstra, n.d.)

现在的编程语言并不关注逻辑。他们专注于逻辑之下的机制;他们称布尔否定是最简单的运算符,必须从一开始就存在,但negative trait bounds(可以理解为否定的模式匹配或者模板,参考这里–译者注)被认为是一个有争议的概念,具有"很多问题"。大多数主流PL在其标准库中支持树数据结构,但sum types几十年来一直未实现。我无法想象没有if运算符的单一语言,但只有少数PL具有成熟的trait bounds,更不用说模式匹配了。这是不一致的 - 它迫使软件工程师设计低质量的API,这些API要么动态并公开很少的编译时检查,要么变得静态并试图规避宿主语言的基本限制,从而使它们的使用越来越晦涩难懂。在单个工作解决方案中组合静态和动态也很复杂,因为您无法在静态上下文中调用动态特性。就函数颜色而言,动态颜色为红色,而静态颜色为蓝色。

除了这种不一致之外,我们还有biformity特征。在C++,Haskell和Rust等语言中,这种biformity相当于最反常的形式;您可以将任何所谓的"富有表现力"编程语言视为两种或更多种较小的语言放在一起:C++语言和C++模板/宏,Rust语言和类型级Rust +声明性宏等。使用这种方法,每次在元级别编写某些内容时,您都无法在宿主语言中重用它,反之亦然,从而违反了DRY原则(正如我们将在一分钟内看到的那样)。此外,biformity增加了学习曲线,强化了语言演变,最终导致功能膨胀,只有编码者才能弄清楚代码中发生了什么。看看Haskell中的任何生产代码,你会立即看到那些众多的GHC#LANGUAGE子句,每个子句都表示一个单独的语言扩展:

{-# LANGUAGE BangPatterns               #-}
{-# LANGUAGE CPP                        #-}
{-# LANGUAGE ConstraintKinds            #-}
{-# LANGUAGE DefaultSignatures          #-}
{-# LANGUAGE DeriveAnyClass             #-}
{-# LANGUAGE DeriveGeneric              #-}
{-# LANGUAGE DerivingStrategies         #-}
{-# LANGUAGE FlexibleContexts           #-}
{-# LANGUAGE FlexibleInstances          #-}
{-# LANGUAGE GADTs                      #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE NamedFieldPuns             #-}
{-# LANGUAGE OverloadedStrings          #-}
{-# LANGUAGE PolyKinds                  #-}
{-# LANGUAGE RecordWildCards            #-}
{-# LANGUAGE ScopedTypeVariables        #-}
{-# LANGUAGE TypeFamilies               #-}
{-# LANGUAGE UndecidableInstances       #-}
{-# LANGUAGE ViewPatterns               #-}

当宿主语言不能提供方便开发所需的足够静态功能时,一些程序员会特别疯狂(insane!),在现有语言之上创建全新的编译时元语言compile-time metalanguageseDSL。因此,不一致性具有转换为biformity的危险属性:
【C++】我们有模板元编程库,如Boost/Hana和Boost/MPL,它们复制了C++的功能,以便在元级别使用:

BOOST_HANA_CONSTANT_CHECK(
    hana::take_while(hana::tuple_c<int, 0, 1, 2, 3>, hana::less.than(2_c))
    ==
    hana::tuple_c<int, 0, 1>
);
constexpr auto is_integral =
    hana::compose(hana::trait<std::is_integral>, hana::typeid_);

static_assert(
    hana::filter(hana::make_tuple(1, 2.0, 3, 4.0), is_integral)
    == hana::make_tuple(1, 3), "");
static_assert(
    hana::filter(hana::just(3), is_integral)
    == hana::just(3), "");
BOOST_HANA_CONSTANT_CHECK(
    hana::filter(hana::just(3.0), is_integral) == hana::nothing);
typedef vector_c<int, 5, -1, 0, 7, 2, 0, -5, 4> numbers;
typedef iter_fold<
    numbers,
    begin<numbers>::type,
    if_<less<deref<_1>, deref<_2>>, _2, _1>
>::type max_element_iter;

BOOST_MPL_ASSERT_RELATION(
    deref<max_element_iter>::type::value, ==, 7);

【c】我自己的编译时元编程框架 Metalang99 通过使用 C 预处理器 (ab) 来做同样的事情。它发展到如此程度,以至于我被迫通过类似Lisp的trampolinecontinuation-passing style(CPS)技术的组合来重新实现递归。最后,我在标准库中拥有了大量列表操作函数,例如ML99_listMapML99_listIntersperseML99_listFoldr,可以说这使得Metalang99作为一种纯数据转换语言,比C本身更具表现力。
【rust】在不一致性Automobile 的第一个示例中,我们使用了 Frunk 库中的hlist。不难看出,Frunk 复制集合和迭代器的某些功能只是为了将它们提升到type-level。应用Iterator::mapIterator::interspersehlist可能很酷,但我们不能。更糟糕的是,如果我们仍然想要执行声明式的数据转换,我们必须保持迭代器适配器与类型级适配器之间的1对1对应关系;每次为迭代器实现新实用程序时,我们都会在hlist中缺少一个实用程序。
【rust】Typenum是另一个流行的类型级库:它允许在编译时通过将整数编码为泛型来执行整数计算。通过这样做,负责整数的语言部分在静态中找到它的对应物,从而引入了更多的biformity。我们不能只是用 参数化某些类型用(2 + 2) * 5,我们必须写这样的东西<<P2 as Add<P2>>::Output as Mul<P5>>::Output!你能做的最好的事情就是编写一个宏,为你完成肮脏的工作,但它只会是语法糖 - 无论如何,你会看到大量具有上述特征的编译时错误。
请添加图片描述
有时,软件工程师发现他们的语言太原始,即使在动态代码中也无法表达他们的想法。但他们并没有放弃:
[Golang]Kubernetes是Golang中最大的代码库之一,在运行时包中实现了自己的面向对象类型系统。
[C] VLC 媒体播放器具有用于表示媒体编解码器的基于宏的插件 API。以下是 Opus 的定义
[C] QEMU 计算机仿真器基于其自定义对象模型构建 QObject``QNum``QNull``QList``QString``QDictQBool
回想一下著名的Greenspun第十条规则(是的!就是我们都知道的那个“任何足够复杂的C或Fortran程序都包含一个临时的,非正式指定的,错误缠身的,缓慢的通用Lisp的一半实现。”–译注),这种手工制作的元语言通常是"临时的,非正式指定的,错误缠身的,缓慢的",具有相当模糊的语义和可怕的文档。元语言抽象的概念根本行不通,尽管创建高度声明性的、小的领域特定语言的理由乍一看听起来很酷。当一个问题实体(或一些中间机制)用宿主语言表示时,你需要了解如何将调用链接在一起以完成工作 - 这就是我们通常所说的API;但是,当这个API是用另一种语言编写的,那么,除了调用序列之外,你还需要了解该语言的语法和语义,这是非常不幸的,原因有两个:它给开发人员带来的心理负担,以及能够支持这种元语言的开发人员数量非常有限。根据我的经验,手工制作的元语言学往往会迅速失控并传播到整个代码库中,从而使它更难挖掘。不仅推理受损,编译器与开发人员的交互也受损:您是否尝试过使用复杂的类型或宏 API?如果是,那么您应该完全熟悉难以理解的编译器诊断,可以在以下屏幕截图中总结:
在这里插入图片描述
这么说很可悲,但现在似乎"富有表现力"的PL意味着"嘿,我严重搞砸了feature的数量,但这没关系!"

最后,关于主语言中的元编程,必须说一句话。使用 Template Haskell 和 Rust 的程序宏等模板系统,我们可以使用同一种语言来处理宿主语言的AST,这在biformity方面是好的,但在语言不一致方面是不愉快的。宏不是函数:我们不能部分应用宏并获得部分应用的函数(反之亦然),因为它们只是不同的概念 - 如果我们要设计一个通用且易于使用的库API,这可能会让人感到痛苦。就个人而言,我确实认为 Rust 中的过程宏是一 个巨大的设计错误 ,可以与纯 C 中的#define宏相媲美:除了纯语法之外,宏系统根本不知道使用的语言 ;你得到的式稍微增强的文本替换而不是一个工具来优雅地扩展和使用一种语言。例如,假设有一个名为Either的枚举,其定义如下:

pub enum Either<L, R> {
    Left(L),
    Right(R),
}

现在想象一下我们有一个任意的特质Foo,并且我们愿意在Either<L,R>实现这个特质,在LR两者都实现。事实证明,我们无法将派生宏应用于Either实现此trait,即使名称是已知的,因为为了做到这一点,此宏必须知道Foo的所有签名。更糟糕的是,Foo可能会在单独的库中定义,这意味着我们不能使用Either<L,R>派生所需的额外元信息来增强其定义。虽然它可能看起来像是一种罕见的情况,但实际上它不是;我强烈建议您查看tokio-util的Either,这是完全相同的枚举,但它实现了Tokio特定的traits,例如AsyncRead AsyncWrite AsyncSeek等。现在想象一下,您的项目中有五个不同的来自不同库的Either这将是脑壳疼的合集!虽然类型内省(运行时检查对象类型或者属性的能力,可能你更熟悉“反射”?–译注)可能是一种妥协,但它仍然会使语言比现在更加复杂。

Idris:The way out?

One of the most fundamental features of Idris is that types and expressions are part of the same language – you use the same syntax for both.
Idris最基本的特征之一是类型和表达式是同一种语言的一部分 - 两者使用相同的语法。
Edwin Brady, the author of Idris (Edwin Brady, n.d.)

让我们思考一下如何解决这个问题。如果我们使我们的语言完全动态化,我们将不会有biformity和inconsistency的问题,但很快就会失去编译时验证的能力,然后在必须要在半夜调试我们的程序。动态类型系统的痛苦是众所周知的。

解决这个问题的唯一方法是使一种语言的功能既是静态的又是动态的,而不是将相同的功能分成两部分。因此,理想的语言抽象既是静态的,也是动态的。但是,它仍然是一个单一的概念,而不是两个逻辑上相似但是具有不同的接口的系统。一个完美的例子是CTFE,俗称constexpr:相同的代码可以在静态上下文下的编译时执行,在动态上下文下的运行时执行(例如,当请求用户输入时)。因此,我们不必为编译时(静态)和运行时(动态)编写不同的代码,而是使用相同的表示形式。

我看到的一个可能的解决方案是dependent types(依赖于值的类型,对应于谓词逻辑中的全称量词和存在量词,强函数式编程的依赖类型不是图灵完全的,反之则无法解决停机问题–译注)。对于依赖类型,我们不仅可以使用其他类型参数化类型,还可以使用值参数化类型。在依赖类型语言Idris中,有一种类型叫做Type - 它代表"所有类型的类型",从而削弱了type-level和value-level之间的二分法。有了如此强大的功能,我们可以表达类型化抽象,这些抽象通常要么内置到语言编译器/环境中,要么通过宏完成。也许最常见和最具描述性的例子是类型安全的printf,它可以动态计算其参数的类型,所以让我们在伊德里斯(Idris)中掌握它的乐趣!

首先,定义归纳数据类型fmt以及从格式字符串获取它的方法:

data Fmt = FArg Fmt | FChar Char Fmt | FEnd

toFmt : (fmt : List Char) -> Fmt
toFmt ('*' :: xs) = FArg (toFmt xs)
toFmt (  x :: xs) = FChar x (toFmt xs)
toFmt [] = FEnd

稍后,我们将使用它来为我们的printf函数生成一个类型。语法与 Haskell 非常相似,读者应该可以理解。

现在最有趣的部分:

PrintfType : (fmt : Fmt) -> Type
PrintfType (FArg fmt) = ({ty : Type} -> Show ty => (obj : ty) -> PrintfType fmt)
PrintfType (FChar _ fmt) = PrintfType fmt
PrintfType FEnd = String

这个函数是做什么的?它根据输入参数fmt计算类型。像往常一样,我们将案例fmt分为三个案例并分别处理它们:

  1. (FArg fmt).由于FArg指示我们将提供可打印的参数,因此这种情况会生成一个采用附加参数的类型签名:
    1. {ty : Type}意味着Idris将自动推导出ty此参数的一种类型(隐式参数)。
    2. Show ty是一个类型约束,它说ty应该实现Show
    3. (obj : ty)是我们必须提供给printf的可打印参数。
    4. PrintfType fmt是处理其余输入fmt的递归调用。在Idris,递归类型由递归函数管理!
  2. (FChar _ fmt).表示格式字符串中的普通字符,因此在这里我们忽略它并继续使用FCharPrintfType fmt
  3. FEnd.这是输入的结束。由于我们希望printf生成一个String,因此我们返回String作为普通类型。

现在假设我们有一个格式字符串"*x*"FArg (FChar ('x' (FArg FEnd)));PrintfType将生成什么类型?很简单:

1.FArg:{ty : Type} -> Show ty => (obj : ty) -> PrintfType (FChar ('x' (FArg FEnd)))
2. FChar:{ty : Type} -> Show ty => (obj : ty) -> PrintfType (FArg FEnd)
3. FArg:{ty : Type} -> Show ty => (obj : ty) -> {ty : Type} -> Show ty => (obj : ty) -> PrintfType FEnd
4. FEnd:{ty : Type} -> Show ty => (obj : ty) -> {ty : Type} -> Show ty => (obj : ty) -> String
很酷,现在是时候实现我们梦寐以求的printf了:

printf : (fmt : String) -> PrintfType (toFmt $ unpack fmt)
printf fmt = printfAux (toFmt $ unpack fmt) [] where
    printfAux : (fmt : Fmt) -> List Char -> PrintfType fmt
    printfAux (FArg fmt) acc = \obj => printfAux fmt (acc ++ unpack (show obj))
    printfAux (FChar c fmt) acc = printfAux fmt (acc ++ [c])
    printfAux FEnd acc = pack acc

如您所见,PrintfType (toFmt $ unpack fmt)出现在类型签名中,这意味着整个printf类型的类型取决于输入参数fmt!但unpack fmt是什么意思呢?由于printf使用fmt:String,我们应该事先将其转换为List Char ,因为我们在toFmt中匹配此字符串;据我所知,伊德里斯不允许以同样的方式匹配String。同样,我们在调用之printfAux前做unpack fmt,因为它也需要List Char作为结果的加和。

让我们来检查printfAux的实现:

  1. (FArg fmt).在这里,我们返回一个 lambda 函数,该函数接受obj并调用show,然后由++运算符追加到acc
  2. (FChar c fmt).只需附加cacc 并在fmt中再次调用printfAux
  3. FEnd.虽然acc是一种List Char,但我们必须返回String(根据PrintfType的最后一种情况),我们在它上面调用pack

最后,测试printf:

printf.idr

main : IO ()
main = putStrLn $ printf "Mr. John has * contacts in *." 42 "New York"

这将打印Mr. John has 42 contacts in "New York".但是,如果我们不提供42呢?

Error: While processing right hand side of main. When unifying:
?ty -> PrintfType (toFmt [assert_total (prim__strIndex “Mr. John has * contacts in *.” (prim__cast_IntegerInt (natToInteger (length “Mr. John has * contacts in *.”)) - 1))])
and:
String
Mismatch
between: ?ty -> PrintfType (toFmt [assert_total (prim__strIndex “Mr. John has * contacts in *.” (prim__cast_IntegerInt (natToInteger (length “Mr. John has * contacts in *.”)) - 1))]) and String.
test:21:19–21:68
17 | printfAux (FChar c fmt) acc = printfAux fmt (acc ++ [c])
18 | printfAux FEnd acc = pack acc
19 |
20 | main : IO ()
21 | main = putStrLn $ printf “Mr. John has * contacts in *.” “New York”
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Warning: compiling hole Main.main

是的,Idris 检测到错误并产生类型不匹配!这基本上就是使用first-class类型实现类型安全的printf的方式。如果你对 Rust 中的相同内容感到好奇,可以看看 Will Crichton 的尝试,它在很大程度上依赖于我们上面看到的异构列表。这种方法的缺点现在应该非常清楚:在 Rust 中,类型系统的语言与主要语言不同,但在 Idris 中,它确实是一回事 – 这就是为什么我们可以自由地将type-level函数定义为返回a类型的常规函数,并在稍后的类型签名中调用它们。此外,由于 Idris 是依赖类型的,您甚至可以根据某些运行时参数计算类型,这在 Zig 等语言中是不可能的。
在这里插入图片描述
我已经预料到了这个问题:使用宏实现printf的问题是什么?毕竟,println!在 Rust 中工作得很好。问题在于宏。想想看:为什么编程语言需要繁重的宏?因为我们可能想要扩展它。我们为什么要扩展它?因为编程语言不符合我们的需求:我们不能使用常规的语言抽象来表达某些东西,这就是为什么我们决定用临时元抽象来扩展语言。在主要部分中,我提供了一个论证,为什么这种方法很糟糕 - 因为宏系统对语言运行机制没有任何线索 ;事实上,Rust 中的过程宏只是 M4 预处理器的一个奇特名称。你们把M4整合到你们的语言中。当然,这比外部M4更好,但它仍然是20世纪的方法。另外,宏甚至不能操作抽象语法树,因为 syn::Item是用于编写过程宏的常见结构,它确实被称为具体的语法树或"解析树"。另一方面,类型是宿主语言的自然组成部分,这就是为什么如果我们可以使用类型来表达编程抽象,我们就会重用语言抽象,而不是诉诸于临时机制。理想情况下,编程语言应该没有宏,或者只有轻量级的语法重写规则(如Scheme的扩展语法或Idris的语法扩展),以保持语言的一致性并非常适合解决预期的任务。

话虽如此,伊德里斯通过引入"所有类型的类型"Type来消除第一个biformity"值泛型"values-generics。通过这样做,它还解决了许多其他对应关系,例如递归与类型级归纳,函数与trait机制等;反过来,这允许尽可能多地使用相同的语言进行编程,即使在处理高度通用的代码时也是如此。例如,您甚至可以将类型列表表示为List Type,就像List NatList String一样,并像往常一样处理它!这可能是由于cumulative hierarchty of universes(见下)。由于 Data.List 的泛型名称a是"隐式"类型Type的,它如同Type,可以是Nat或者String ;在后一种情况下,a将推导出为Type 1。需要这样一个无限的类型序列来避免罗素悖论的变化,使inhabitant"结构上小于"其类型。
然而,Idris并不是一种简单的语言。我们的二十行printf示例已经使用了"整个 lotta 功能",例如归纳数据类型、依赖模式匹配、隐式、类型约束等。此外,Idris还具有计算效应,阐述器反射,共感数据类型以及用于定理证明的多多东西。有了如此多的工具,你通常在摆弄你的语言,而不是做一些有意义的工作。我很难相信,在他们目前的状态下,依赖型语言会找到大量的生产用途;至于现在,在编程世界中,对于PL研究人员和像我这样的随机爱好者来说,它们只不过是一件花哨的事情。依赖类型本身太低级了。
在这里插入图片描述

Zig:Simpler,but to systems

In Zig, types are first-class citizens. They can be assigned to variables, passed as parameters to functions, and returned from functions.
在Zig中,类型是一等公民。它们可以分配给变量,作为参数传递给函数,并从函数返回。
The Zig manual (Zig developers, n.d.)

我们的最后一个病人是Zig编程语言。以下是 Zig 中printf的编译时实现(抱歉,目前尚未突出显示):

const std = @import("std");

fn printf(comptime fmt: []const u8, args: anytype) anyerror!void {
    const stdout = std.io.getStdOut().writer();

    comptime var arg_idx: usize = 0;

    inline for (fmt) |c| {
        if (c == '*') {
            try printArg(stdout, args[arg_idx]);
            arg_idx += 1;
        } else {
            try stdout.print("{c}", .{c});
        }
    }

    comptime {
        if (args.len != arg_idx) {
            @compileError("Unused arguments");
        }
    }
}

fn printArg(stdout: std.fs.File.Writer, arg: anytype) anyerror!void {
    if (@typeInfo(@TypeOf(arg)) == .Pointer) {
        try stdout.writeAll(arg);
    } else {
        try stdout.print("{any}", .{arg});
    }
}

pub fn main() !void {
    try printf("Mr. John has * contacts in *.\n", .{ 42, "New York" });
}

在这里,我们使用一个名为comptime的功能:comptime函数参数意味着在编译时必须知道它。它不仅允许积极的优化,而且还打开了一个"元编程"设施的神殿,最值得注意的是没有单独的宏观级别或类型级子语言。上面的代码不需要进一步的解释 - 每个程序员都应该清楚的简单的逻辑,不像是printf.idr看起来像是一个疯狂天才的幻想的果实。

如果我们省略42,Zig 将报告编译错误:

An error occurred:
/tmp/playground2454631537/play.zig:10:38: error: field index 1 outside tuple 'struct:33:52' which has 1 fields
            try printArg(stdout, args[arg_idx]);
                                     ^
/tmp/playground2454631537/play.zig:33:15: note: called from here
    try printf("Mr. John has * contacts in *.\n", .{ "New York" });
              ^
/tmp/playground2454631537/play.zig:32:21: note: called from here
pub fn main() !void {

在开发printf过程中,我遇到的唯一不便是巨大的错误…很像C++模板。但是,我承认这可以通过更明确的类型约束来解决(或至少能够面面俱到)。总的来说,Zig的类型系统的设计似乎是合理的:有一种所有类型的类型叫做type,并且使用comptime,我们可以通过常规变量,循环,过程等在编译时计算类型。我们甚至可以通过@typeInfo@typeName@TypeOf内置来执行类型反射!是的,我们不能再依赖于运行时值,但是如果您不需要定理证明器(theorem prover),那么完整的依赖类型可能有点过分了。
请添加图片描述
一切都很好,除了Zig是一种系统语言。在他们的官方网站上,Zig被描述为"通用编程语言",但我很难同意这种说法。是的,您几乎可以用Zig编写任何软件,但是您应该这样做吗?我在 Rust 和 C99 中维护高级代码的经验表明"不"。第一个原因是安全性:如果你使系统语言安全,你将使程序员处理与业务逻辑完全无关的借用检查器borrow checker和所有权(或等效)问题(相信我,我知道这有多痛苦);如果您选择C方式的手动内存管理,您将让程序员长时间调试他们的代码,并希望-fsanitize=address能够显示一些有意义的东西。此外,如果要在指针之上构建新的抽象,则最终会得到 &str, AsRef<str>, Borrow<str>, Box<str>类似的抽象。拜托,我只想要一个UTF-8字符串;大多数时候,我并不真正关心它是否是这些替代方案之一。

第二个原因与语言运行时有关:对于一种语言来说,为了避免隐藏的性能损失,它应该有一个最小的运行时 - 没有默认的GC,没有默认的事件循环等,但对于特定的应用程序,可能有必要有一个运行时 - 例如,处理异步运行时,所以实际上你必须以某种方式处理自定义运行时代码。在这里,我们遇到了一组关于函数颜色(见上)的全新问题:例如,在你的语言中没有工具来抽象同步和异步函数意味着你把你的语言分为两部分:同步和异步,比如说,如果你有一个通用的高阶库,它将不可避免地被标记为async以接受各种用户回调。为了解决这个问题,你需要实现某种形式的effect polymorphism(例如,monads或代数效应algebraic effects),这仍然是一个研究课题。高级语言天生需要处理的问题较少,这就是为什么大多数软件都是用Java,C#,Python和JavaScript编写的。在 Golang 中,从概念上讲,每个函数都是async ,因此默认情况下有助于保持一致性,而无需诉诸复杂的类型特征。相反,Rust 已经被认为是一种复杂的语言,仍然没有标准方法来编写真正的通用异步代码。

Zig 仍然可以用于大型系统项目,如 Web 浏览器、解释器和操作系统内核 - 没有人希望这些东西意外冻结。Zig的低级编程功能将促进内存和硬件设备的便捷操作,而其健全的元编程方法(在正确的手中)将培养可理解的代码结构。引入高级代码只会增加精神负担,而不会带来可观的好处。

Progress is possible only if we train ourselves to think about programs without thinking of them as pieces of executable code.
只有当我们训练自己思考程序而不将它们视为可执行代码片段时,进步才有可能。
Edsger Dijkstra

结语

静态语言强制执行编译时检查;这很好。但它们存在特征biformity和inconsistency - 这很糟糕。另一方面,动态语言在较小程度上遭受了这些缺点的影响,但它们缺乏编译时检查。假设的解决方案应该从两全其美中取出最好的东西。

编程语言应该被重新考虑。

补充

补充一些语言feature的介绍

borrow

借用,出现于rust
借用只能或者是对资源的一个或者多个引用或者是一个可变引用
borrow的scope小于所有者的scope

元组结构体

出现于rust中,
形式是元组的结构体,它存在的意义是为了处理那些需要定义类型(经常使用)又不想太复杂的简单数据:

struct Color(u8, u8, u8);
struct Point(f64, f64);

let black = Color(0, 0, 0);
let origin = Point(0.0, 0.0);

PhantomData

只在Rust中出现
PhantomData是一个零大小类型的标记结构体。

作用:
并不使用的类型;
型变;
标记拥有关系;
自动trait实现(send/sync);

不透明类型和协议类型

以swift为例
协议类型:支持一组方法的类型
不透明类型:隐藏返回值的类型信息,编译器可以访问,但是客户端不能访问

implicit

以Scala为例
scala 用implicit来隐式传递参数,包括函数的隐式值,隐式视图(用来做参数类型的隐式转换),隐式转换(调用类中本来不存在的方法)

traits

以rust为例

case classes

以Scala为例
case class擅长为immutable数据建模。
case class有一个apply默认方法用于实例化case classes类。
case class按照结构而非数据进行比较。

Scala Monoid

以scala为例
幺半群
Monoid(幺半群)是一个带有满足结合律的二元运算和单位元的集合。

Scala Context Bounds

Scala 2.8引入的新特性,通常与类型类模式type class pattern一起使用

//等价于
def foo[A](a:A)(implicit b:B[A]) = g(a)
// 将B折叠到A做隐式值传递
def foo[A : B](a: A) = g(a) 

因为使用了context bound之后implicit参数值不能显式传递,所以需要使用implicitly标识符获取上下文中类型的隐式值

  def fol1[F[_], A](list: F[A])(m: Monoid[A])(implicit f: Foldable[F]): A = {
    f.foldleft(list)(m.zero)(m.combine)
  }
//-->>
def fold[F[_]: Foldable, A](list: F[A])(m: Monoid[A]): A = {
  implicitly[Foldable[F]].foldleft(list)(m.zero)(m.combine)
}
// impliit参数被隐式传递了

异构(IsoMerism)

这段代码没看懂

// A pair of arbitrary case classes
case class Foo(i : Int, s : String)
case class Bar(b : Boolean, s : String, d : Double)

// Publish their `HListIso`'s
implicit def fooIso = Iso.hlist(Foo.apply _, Foo.unapply _)
implicit def barIso = Iso.hlist(Bar.apply _, Bar.unapply _)

// And now they're monoids ...

implicitly[Monoid[Foo]]
val f = Foo(13, "foo") |+| Foo(23, "bar")
assert(f == Foo(36, "foobar"))

implicitly[Monoid[Bar]]
val b = Bar(true, "foo", 1.0) |+| Bar(false, "bar", 3.0)
assert(b == Bar(true, "foobar", 4.0))
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
网管教程 从入门到精通软件篇 ★一。★详细的xp修复控制台命令和用法!!! 放入xp(2000)的光盘,安装时候选R,修复! Windows XP(包括 Windows 2000)的控制台命令是在系统出现一些意外情况下的一种非常有效的诊断和测试以及恢复系统功能的工具。小编的确一直都想把这方面的命令做个总结,这次辛苦老范给我们整理了这份实用的秘笈。   Bootcfg   bootcfg 命令启动配置和故障恢复(对于大多数计算机,即 boot.ini 文件)。   含有下列参数的 bootcfg 命令仅在使用故障恢复控制台时才可用。可在命令提示符下使用带有不同参数的 bootcfg 命令。   用法:   bootcfg /default  设置默认引导项。   bootcfg /add    向引导列表中添加 Windows 安装。   bootcfg /rebuild  重复全部 Windows 安装过程并允许用户选择要添加的内容。   注意:使用 bootcfg /rebuild 之前,应先通过 bootcfg /copy 命令备份 boot.ini 文件。   bootcfg /scan    扫描用于 Windows 安装的所有磁盘并显示结果。   注意:这些结果被静态存储,并用于本次话。如果在本次话期间磁盘配置发生变化,为获得更新的扫描,必须先重新启动计算机,然后再次扫描磁盘。   bootcfg /list   列出引导列表中已有的条目。   bootcfg /disableredirect 在启动引导程序中禁用重定向。   bootcfg /redirect [ PortBaudRrate] |[ useBiosSettings]   在启动引导程序中通过指定配置启用重定向。   范例: bootcfg /redirect com1 115200 bootcfg /redirect useBiosSettings   hkdsk   创建并显示磁盘的状态报告。Chkdsk 命令还可列出并纠正磁盘上的错误。   含有下列参数的 chkdsk 命令仅在使用故障恢复控制台时才可用。可在命令提示符下使用带有不同参数的 chkdsk 命令。   vol [drive:] [ chkdsk [drive:] [/p] [/r]   参数  无   如果不带任何参数,chkdsk 将显示当前驱动器中的磁盘状态。 drive: 指定要 chkdsk 检查的驱动器。 /p   即使驱动器不在 chkdsk 的检查范围内,也执行彻底检查。该参数不对驱动器做任何更改。 /r   找到坏扇区并恢复可读取的信息。隐含着 /p 参数。   注意 Chkdsk 命令需要 Autochk.exe 文件。如果不能在启动目录(默认为 %systemroot%System32)中找到该文件,将试着在 Windows 安装 CD 中找到它。如果有多引导系统的计算机,必须保证是在包含 Windows 的驱动器上使用该命令。 Diskpart   创建和删除硬盘驱动器上的分区。diskpart 命令仅在使用故障恢复控制台时才可用。   diskpart [ /add |/delete] [device_name |drive_name |partition_name] [size]   参数 无   如果不带任何参数,diskpart 命令将启动 diskpart 的 Windows 字符模式版本。   /add   创建新的分区。   /delete   删除现有分区。   device_name   要创建或删除分区的设备。设备名称可从 map 命令的输出获得。例如,设备名称:   DeviceHardDisk0   drive_name   以驱动器号表示的待删除分区。仅与 /delete 同时使用。以下是驱动器名称的范例:   D:   partition_name   以分区名称表示的待删除分区。可代替 drive_name 使用。仅与 /delete 同时使用。以下是分区名称的范例:   DeviceHardDisk0Partition1    大小   要创建的分区大小,以兆字节 (MB)表示。仅与 /add 同时使用。   范例   下例将删除分区: diskpart /delete Device HardDisk0 Partition3 diskpart /delete F:   下例将在硬盘上添加一个 20 MB 的分区:   diskpart /add Device HardDisk0 20   Fixboot
CRT (C Runtime Library) 是一个用于支持 C 语运行时环境的库。在 Linux 系统中,CRT 通常是由 glibc (GNU C Library) 提供的。加载 CRT 可能需要花费一定的时间,这取决于多个因素,包括系统性能、库的大小和复杂性等。 以下是一些可能导致加载 CRT 时间较长的原因: 1. 系统性能:如果系统资源有限或者负载较高,加载库的速度可能受到影响,从而导致加载 CRT 的时间延长。 2. 库的大小和复杂性:CRT 是一个相对庞大和复杂的库,其中包含了许多与 C 语言运行时相关的功能和支持。加载这样的库可能需要较长的时间。 3. 磁盘访问速度:如果 CRT 存储在磁盘上,加载库可能受到磁盘访问速度的限制。较慢的磁盘速度可能导致加载时间延长。 为了加快加载 CRT 的速度,你可以考虑以下几点: 1. 确保系统资源充足:确保系统有足够的内存和处理能力来处理加载库的任务。 2. 使用更快的存储介质:如果可能的话,将 CRT 存储在更快的存储介质上,如固态硬盘 (SSD)。 3. 优化编译参数:在编译应用程序时,可以尝试使用一些优化参数来减少库的大小,从而减少加载时间。例如,使用 `-Os` 参数可以优化库的大小。 4. 使用静态链接:如果允许的话,可以考虑将 CRT 静态链接到应用程序中,这样可以避免在运行时加载库的时间。 需要注意的是,加载 CRT 的时间可能受到多个因素的影响,并且可能因系统和环境的不同而有所差异。因此,加载 CRT 时间较长并不一定是异常情况,除非存在明显的性能问题或错误。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值