类型推断技术及仓颉语言实践

news2024/11/15 11:18:49

史磊

仓颉语言类型推断技术专家

一、一种看待类型系统的方式

一门编程语言一定得包含类型系统吗?

这个问题今天看来可能显而易见,一个程序没有类型的话还能算是个完整、正确的程序吗?但是其实关于类型系统的作用,一直是存在两种针锋相对的看法的。大家普遍习惯的这种把类型作为程序不可分割的一部分的看待方式叫做“内生”(intrinsic)的理解方式,即一个程序如果没有合法的类型就不能算有意义的程序。而与之相对的,也存在一种“外生”(extrinsic)的理解方式,持这种观点的人认为,程序即使没有类型也同样有意义,类型检查只是额外地证明了这个程序的一些性质,与其他的各种程序分析工具应该处于同样的地位。

姑且不论类型检查是否应该作为一个语言之外的的工具这种工程问题,顺着“类型检查是为了证明了程序的一些性质”这个思路,我们可以得到一些很有趣的认知。

不妨想象一下,一个程序如果在没有类型的情况下就执行起来,会发生什么问题呢?我们有可能给一个函数传入一个整数,但是这个函数实际上却是处理字符串用的,于是它错误地把这个整数形式地数据当做字符串来处理了,最后自然是得不到期望的结果。例如下面的伪代码:

func f(s) {
    s.replace(...)
}
​
let s = f(1)

而在这个过程中加入类型系统的话,如果可以静态地验证这个被调用的函数的形参类型是一个字符串,而每次调用它时实参类型都也是字符串,那么我们就可以保证,在任何情况下执行这个程序,都不会出现把一个整数类型的参数传递给这个处理字符串的函数这种低级错误。

所以这里我们用比较朴素的方式去理解的话,类型系统、尤其是静态类型系统,实际上是一种“程序在运行时处理数据的方式永远不会出错”的证明,如果类型检查可以通过,那么每个数据被传递时,它的提供者和接收者对于这块数据的格式、可以被执行的操作等等的理解总是一致的。实际上,这个性质已经几乎就是一些比较公认的“类型安全”(type safety)的定义了。

另外细心的读者可能会注意到,既然类型系统是一种程序运行时“永远不会出错”的证明,那这个证明完成后,在程序运行时,我们还需要保留“类型”这个东西吗?确实如此,除了为了支持动态派遣等一些动态特性外,大多数静态类型的信息在类型检查完成后对于程序的正确运行就基本没有用处了,理论上是可以被擦除掉的。当然,实际的实现中类型信息对编译优化也有帮助,所以也有语言会选择在后续的编译阶段一直保留它们。

那么我们再回头去看这篇文章最关心的重点问题:类型推断是什么?类型标注又是什么?既然类型检查是一种正确性的证明,那开发者提供的类型标注自然就是手写的部分证明,而类型推断则是一种自动化证明的技术。自动化证明的算法越聪明,必须手写的部分也就越少,能支持的类型特性也就越丰富。实际上,在一些研究性的语言中,类型系统早已超越简单的“处理数据的方式”的正确性证明,还可以包括资源消耗、执行时长、数组长度、值的可空性等等多种方面的正确性证明,其中的类型推断用到的技术与更一般的形式化证明也越来越接近了。

二、仓颉的类型推断需要做什么

在仓颉语言中,类型推断主要起到两个作用,第一是确定每个声明和表达式的类型,这既包括可以省略的变量类型、函数返回类型、泛型实参类型等等,也包括分支、循环、字面量等并不涉及类型标注的各种表达式的类型。第二是帮助确定每个被使用的名字对应的声明,最典型的情况是在调用一个重载的函数时,需要确定调用的具体是哪个定义,另外有些不同种类的定义,如果碰巧名字相同,我们也可能需要借助类型信息来确定使用的究竟是哪个。

三、基于合一(unification)的类型推断

在一些早期的编程语言中,类型之间并没有子类关系,只有相等或不等。时至今日,多数函数式编程语言仍然继承了这个设计思路,包括 Rust 语言如果不考虑生命周期的话,类型之间也没有子类关系。在这些语言中,类型推断普遍是基于合一(unification)过程来完成的。简单来说,即是每当我们遇到一个暂不确定的类型,那就为它引入一个待解的变元,然后在遍历程序的过程中,不断建立起类型变元之间的方程组(如一个函数的形参类型和实参类型必须相等,由此就可以得到一个新的方程),并不断简化这些方程,直到所有类型变元都可以求解出来(或者缺乏足够的信息来求解,此时就会将这样的类型变元泛化,不过这超出本文的讨论范围了),类型推断的过程十分类似于数学上解方程的过程。

这种做法的好处是,这些方程组是从整个程序里所有信息中建立起来的,所以每个未知类型都可以利用全局信息去求解,解出的结果也可以保证是全局正确的。

例如下面这个程序在 Rust 中是可以编译通过的,它可以根据第 5 行的函数调用解出变量 v 需要有 u16 类型。但是等价的程序在一些没有全局的类型推断的语言中则无法通过编译,因为在第 4 行中 v 的类型就已经被推断为整数字面量的默认类型了,这通常是 u64

fn f(x:u16) {}
​
fn main() {
    let v = 1;
    f(v);
}

那么为什么不是所有语言都用这种基于合一(unification)的类型推断方式呢?主要问题就出在子类关系上。

如果类型间不存在子类关系,那么所有类型实际上是一些离散的点,两个类型要么相等,要么不等,几乎没有歧义的空间。这样求解方程非常简单直接,结果也很容易保证完备性和可靠性。

但是有了子类关系,类型之间就不再是一些毫无关系的点了,而是形成了一个偏序集。从程序中可以建立起的也不再是基于类型相等的方程组,而是基于子类关系的不等式组。求解一个偏序集上的不等式组则是一个复杂得多的问题,很多时候我们甚至无法在可接受的时间内求得一个完全精确的解,只能退而求其次做一些近似。这就导致基于合一(unification)的类型推断在有子类关系的语言中非常难以使用,而仓颉语言是有子类关系的,所以也暂时没有采用这种类型推断方式。

四、局部类型推断

上面讲的的子类关系会导致的问题,曾一度导致在有子类关系的语言中做类型推断的研究非常稀少。对于注重实用的工业语言来说,比较重要的一篇研究论文是 Benjamin. C. Pierce 等人著的 《Local Type Inference》1。它不再纠结于全局的完全性和完备性,退而求其次只追求每个表达式的类型在相邻表达式能提供的信息中保证正确。仓颉的类型推断基本上是基于这篇论文的框架在实现。

更准确地来说,《Local Type Inference》这篇论文实际上包含了两个类型推断的思想,一个是仅依据局部信息做类型推断,也就是“局部类型推断”,另一个是类型信息应该在表达式中自底向上和自顶向下双向传播,也叫“双向类型推断”。这两者在仓颉的类型推断实现中都有所体现,下面就具体来介绍一下仓颉的类型推断方式:

局部元素

在一个块内部,编译器会按从上到下的顺序依次处理各个表达式和局部声明。

在一个表达式或局部声明的内部,编译器依据可获得的类型信息,会做自顶向下或者自底向上的推断。

自顶向下的推断是指,父表达式如果对子表达式有期望的类型,则我们会借助这个期望类型的信息去推断子表达式的类型。一个典型的情形是,在一个函数调用表达式中,我们总是期望实参的类型是对应位置形参的子类,因而可以用形参类型作为期望类型去帮助推断实参类型。其他还包括 if 表达式的条件需要是 Bool 类型的子类,变量的初始化表达式的类型需要是变量类型的子类等等。

下面举一些例子:

func handleByte(b: Byte) {
}
​
func f() {
    let v: Int32 = 1 // 1 inferred to Int32
    handleByte(2)    // 2 inferred to Byte
​
    let v2 = 3       // 3 inferred to Int64
}

上面的程序中,第5行的变量 v 有类型标注 Int32,因此对其初始化表达式存在期望类型 Int32,所以整数字面量 1 被推断为类型 Int32

第6行的 handleByte 函数有形参类型 Byte,因此这里的调用表达式对它的实参就有期望类型 Byte,所以整数字面量 2 被推断为类型 Byte

以上都是自顶向下的推断发生的场景,而第8行中则没有发生自顶向下的推断。变量 v2 没有标注类型,所以字面量 3 就不具有任何从父表达式得到的期望类型,只能被推断为整数字面量的默认类型 Int64

需要注意的是,自顶向下的期望类型在一些表达式中是可以向下传递的。例如,一个 if 表达式如果本身有期望类型,那么我们会认为它的各个分支同样有这个期望类型。

下面是一个例子:

func narrow(x: Int64):Byte {
    let v: Byte = if (x >= 0) {
        1       // 1 inferred to Byte
    } else {
        -1      // -1 inferred to Byte
    }
    return v
}

其中变量 v 标注了类型 Byte,因此其初始化表达式、即 if 表达式会具有期望类型 Byte。然后 if 的两个分支也都会传递地具有期望类型 Byte。因而,第3行的 1 和第5行的 -1 字面量都将被推断为 Byte 类型。

除了 if 表达式以外,一个表达式的返回值如果可能来源于它的某个子表达式,那么对这个表达式的期望类型通常就可以传递给这个子表达式,如从 match 表达式到它的各个 case,从 try-catch 表达式到它的 try 分支和各个 catch 分支等等。

而自底向上的推断是指,如果父表达式对子表达式没有期望的类型,我们会先推断出子表达式的类型,并反过来用这个信息帮助推断父表达式的类型。一个典型的场景是调用泛型函数时,如果没有写出泛型参数,则通常需要先推断出实参的类型,然后才能依此求解出整个函数调用表达式的泛型参数,进而得到整个表达式的类型。

以下是一个简单的例子:

func id<T>(x: T): T {
    x
}
​
let s = id("hello")     // will infer id<String>

其中定义了一个接受一个任意类型的参数并返回其自身的泛型函数 id。第5行代码中,我们既不知道变量 s 的类型,也不知道调用的 id 函数的类型参数,所以只能先从最里层的表达式 "hello" 开始推断,得到它的类型为 String。然后向上,利用实参类型为 String,求解出此处调用 id 的泛型实参是 String,进而确定它的返回类型也是 String,最后再向上推断出变量 s 的类型是 String

泛型参数求解

在调用泛型函数、或者构造泛型类型时,我们需要知道本次调用的泛型实参。函数调用的泛型实参通常可以省略,此时编译器会尝试从上下文中推断出泛型实参的类型。在局部类型推断中,这是最为复杂的步骤,也是唯一实际上用了合一(unification)的步骤。

上文介绍过,在有子类关系的类型系统上合一(unification)需要求解不等式组。那么我们需要首先建立这些不等式。因为局部类型推断并不考虑全局信息,所以针对一个特定的泛型函数的调用,我们只会考虑下面几个不等式的来源:

  • 函数的形参类型需要是实参类型的父类

  • 函数的返回类型需要是本次调用的期望返回类型的子类

  • 函数的泛型约束需要被满足

具体求解的过程略为复杂,本文不会详细介绍。大致上来说,对于一个泛型参数,编译器会尝试寻找能满足以上所有约束的唯一最具体类型、或唯一最抽象类型,来作为这个泛型参数的解。如果这样的类型无法找到,则会编译失败。另外,如果找到的类型只能是 Any 或者 Nothing,则编译也会失败。

以下是一个简单的情形:

func tryPrint<T>(x: Option<T>) where T <: ToString {
    if (let Some(v) <- x) {
        println("Has value: ${v}")
    }
}
​
main() {
    tryPrint(Some(1))   // will infer tryPrint<Int64>
}

第8行代码中 tryPrint 函数的泛型参数需要推断。它的形参类型和实参类型间的对应关系为 Option<Int64> <: Option<T>,此外它有泛型约束 T <: ToString。结合两者,我们发现类型 Int64可以作为 T 的解,所以此处 tryPrint 的泛型实参就被推断为 Int64

另外在一些较为复杂的情形下,泛型函数的实参之间可能有类型信息的依赖,无法用一次简单的自底向上或者自顶向下推断完成泛型参数的求解。这种情形在原版的《Local Type Inference》中并没有解决,一个比较有名的后续工作叫做《Colored Local Type Inference》2,它通过一个十分重量级的方法 -- 给类型中每个类型参数再更详细地标上需要自底向上还是自顶向下推断出来 -- 来实现对这种情形的支持。

仓颉参考了这种思路,但是并没有引入那么重量级的标记,只是会在泛型函数调用时尝试多次迭代求解泛型参数。以经典的 map 函数为例:

import std.collection.ArrayList
​
func map<T, R>(f: (T)->R, arg: ArrayList<T>): ArrayList<R> {
    let res = ArrayList<R>()
    for (v in arg) {
        res.append(f(v))
    }
    res
}
​
main(){
    let input: ArrayList<Int64> = ArrayList([1, 2, 3])
    let output = map({ x => x.toString() }, input)      // will infer map<Int64, String>
}

map 函数接受一个转换函数和一个列表,将列表中的元素逐一转换后输出新的列表。因为有转换前后两种不同类型,所以它需要2个泛型参数 TR

在第13行对 map 的调用中,泛型参数 T 可以很显然地从 input 参数的类型推断出来为 Int64,但是对它的另一个参数,即作为转换函数的 lambda,在没有期望类型的情况下,我们无法简单确定这个 lambda 的参数和返回类型,因而也就无法解出 R 的类型。

但同时我们又可以观察到,这个 lambda 的参数类型和 input 的类型都对应着泛型参数 T,因此他们应该相等。而知道lambda的参数类型后,他的返回类型就可以推断出来了。进而,R 的类型也可以求解出来。

所以总的来说,这种情形仍然是有足够的信息进行泛型参数的求解的,只不过需要跨越参数进行一些类型信息的传递。

对于这种参数之间有类型信息的依赖的泛型函数调用,我们的处理流程是这样的:

  1. 首先,推断出所有不需要期望类型就可以推断成功的实参的类型

  2. 然后用推断出的实参类型尽可能地求解泛型参数的类型

  3. 将可以求解出的泛型参数代入形参类型中,将这些(可能没有完全实例化的)形参作为期望类型去推断对应的实参类型,这个过程中,我们会忽略掉类型中没有完全实例化的部分

  4. 重复 2、3 步,直到没有新的泛型参数可以被求解(即失败),或者所有泛型参数都可以被求解(即成功)

最后还有一种较为特殊的场景,有些函数可能定义成柯里化的形式,即每次接收一个参数,返回接收下一个参数的函数。

例如,仓颉标准库中的 map 函数就提供了柯里化方式定义的版本:

public func map<T, R>(transform: (T) -> R): (Iterable<T>) -> Iterator<R> {
    return {it: Iterable<T> => it.iterator().map<R>(transform) }
}

柯里化的函数调用时,会分为多次函数调用:

let it = map({x=>x.toString()})([1,2,3])

当然,通常我们写成管道操作符的语法糖形式:

let it = [1, 2, 3] |> map{x=>x.toString()}

这种情形下,柯里化的泛型函数的多次调用是连续发生的,它泛型参数仍然可以被推断出来,编译器会将后面的调用中的实参类型信息以期望的返回类型的形式向前传播,帮助推断泛型参数。

也就是说,对于第一次对 map 的调用 map({x=>x.toString()}),我们可以知道它的返回类型需要满足: (Iterable<T>) -> Iterator<R> <: (Array<Int64>) -> Any。然后使用上面描述过的求解步骤,就可以解出 TR 分别为 Int64String

重载决议

仓颉语言支持函数和操作符的重载,在编译时,需要静态地做重载决议,以确定每个重载函数的调用究竟用了哪个定义。

仓颉做重载决议的过程大致上可以分为3步:

  1. 找出当前(调用)位置可见的候选定义

  2. 检查每个候选定义是否能通过调用处局部的类型检查

  3. 在所有可以通过类型检查的定义中,选取最匹配的一个

更具体的规则可以在用户手册或者语言规约中找到,这里就不再赘述。这里主要讨论2个在重载决议过程中值得一提的点。

第一点是,如果被调用函数的某些候选定义是泛型函数,且需要求解泛型参数,那么对每个候选定义会分别进行泛型参数的求解。

例如在下面的程序中,第8行对 f 的调用,若选取第1行的定义,则可以求解出泛型参数 TString,若选取第4行的定义,则可以求解出泛型参数 TInt64。两者皆可类型检查通过,所以会报错无法决议。

func f<T>(x: Int64, y: T) {
}

func f<T>(x: T, y: String) {
}

main() {
    f(1, "c") // Error, both candidates match, ambiguous
}

第二点则是因为上面提到的双向类型推断而衍生出的问题。

因为我们采用的双向类型推断策略,一个函数调用期望的返回类型对一个候选定义能否通过“局部的类型检查”会产生影响,因而可能改变重载决议的结果。也就是说,一个重载函数的调用,在不同上下文下,决议的结果是可能不同的。

例如下面的程序中,第10行的 f 会考虑到期望的返回类型是 Int32 而选择第5行的定义,而第11行的 f 会考虑到期望的返回类型是 Int64 而选择第1行的定义。

func f(x: Int64):Int64 {
    x
}

func f(x: Int32):Int32 {
    x
}

main() {
    let v1: Int32 = f(1)     // will use definition of line 5
    let v2: Int64 = f(1)     // will use definition of line 1
}

那么我们再考虑一种特殊的情况,如果有多层重载函数的调用嵌套起来,会发生什么事呢?因为外层函数的形参会被视为内层函数调用的期望类型,而外层函数的形参类型会依候选定义的选取而有所变化,所以外层对候选定义的选取同样会影响内层的重载决议。

例如下面的程序中,第10行外层的 widen 会考虑到期望的返回类型是 Int32 而选择第5行的定义,而内层的 widen 会考虑到外层期望的参数类型是 Int16 而选择第1行的定义,最后,字面量 1 会因为第一行的 widen 定义期望一个 Int8 类型的参数而被推断为 Int8 类型。程序整体可以通过类型检查。

func widen(x: Int8):Int16 {
    Int16(x)
}

func widen(x: Int16):Int32 {
    Int32(x)
}

main() {
    let v: Int32 = widen(widen(1))      // 1 inferred to Int8
}

这就产生了一个问题,重载函数嵌套层数较多的情形下(如比较大的UI组件、复杂的算术表达式等),我们做决议可能需要搜索各层的候选组合,而需要搜索的组合数可能随着嵌套层数而指数增长,这就可能让编译时间长到无法接受。实际上同样的问题也可以在 Swift 语言中观察到,例如至少在撰写本文时,Swift 的编译器仍然会因为如下的程序编译超时:

let a: Double = -(1 + 2) + -(3 + 4) + -(5)

这和 Swift 同样使用了双向类型推断不无关系。

那么仓颉是如何解决这个问题的呢?让我们回忆一下,一个函数的重载决议总共需要哪些信息?

  1. 当前的表达式

  2. 对当前表达式的期望类型

  3. 当前上下文中的符号表

其中对于源程序中同一个位置的表达式, 1 是完全不变的,3 可以近似认为不变,事实上会导致指数的搜索量的是 2 在不同上下文中会有所不同。看到这里,经常刷算法题的朋友可能可以敏锐地发现,在双向类型推断下做重载决议实际上几乎是一个动态规划问题。对于给定的程序位置和期望类型,只要上下文中的符号表不发生改变,那么当前位置的表达式能否通过类型检查就是一个具有最优子结构的问题。而所有可能的期望类型并不是很多,它最多等于父表达式调用的函数的候选定义的总数。

因此,我们可以在重载决议时做一个记忆化搜索,对同一个函数调用,如果用同样的期望类型已经做过类型检查,直接复用前一次检查时缓存的结果就好了。而在一些特殊情形下,如果符号表确实发生了变化,只要将受影响的缓存清除掉即可。如此一来,对于绝大多数程序,重载决议都可以在接近多项式的时间复杂度内完成。

六、小结

本文简单介绍了业界对类型系统和类型推断相关的讨论和已有工作,并介绍了在仓颉的实践中类型推断遇到的问题和解决的方式。仓颉使用了基于局部类型推断的算法框架,并针对一些常见的使用场景做了专门的优化,在推断能力、算法复杂度、以及支持的语言特性间达到了一个不错的平衡点。

七、参考文献

  • [1] Pierce B C, Turner D N. Local type inference[J]. ACM Transactions on Programming Languages and Systems (TOPLAS), 2000, 22(1): 1-44.

  • [2] Odersky M, Zenger C, Zenger M. Colored local type inference[J]. ACM SIGPLAN Notices, 2001, 36(3): 41-53.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1971709.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

PCB打板——usb扩展坞

采用一个typec模块&#xff0c;四个typea模块&#xff0c;以及保险丝&#xff0c;电源滤波部分&#xff0c;原理图如下 这里usb信号线为差分信号&#xff0c;要用差分导线&#xff08;长度相同&#xff0c;对称&#xff09;

RK3568笔记五十:SPI通信-回环测试

若该文为原创文章&#xff0c;转载请注明原文出处。 一、SPI引脚关系 其中SPI1的引脚关系如下表所示 SPI 引脚 功能 MOSI GPIO3_C1 主设备输出/从设备输入 MISO GPIO3_C2 主设备输入/从设备输出 CLOCK CPIO3_C3 时钟信号线 CS0 GPIO3_A1 片选信号线0 CS1 NC …

kettle从入门到精通 第八十二课 ETL之kettle kettle中的【阻塞数据直到步骤都完成】使用教程

1、在使用步骤【阻塞数据直到步骤都完成】&#xff08;英文为Block this step until steps finish&#xff09;之前&#xff0c;我们先来了解下什么是 Copy Nr&#xff1f; Copy Nr是指 “副本编号” 或 “拷贝编号”&#xff0c;也就是下图中的复制的记录行数&#xff0c;图中…

全网最强Nginx教程 | 万字长文爆肝Nginx(五)

Nginx实现服务器端集群搭建 Nginx与Tomcat部署 前面课程已经将Nginx的大部分内容进行了讲解&#xff0c;我们都知道了Nginx在高并发场景和处理静态资源是非常高性能的&#xff0c;但是在实际项目中除了静态资源还有就是后台业务代码模块&#xff0c;一般后台业务都会被部署在…

基于Spring boot + Vue的灾难救援系统

作者的B站地址&#xff1a;程序员云翼的个人空间-程序员云翼个人主页-哔哩哔哩视频 csdn地址&#xff1a;程序员云翼-CSDN博客 1.项目技术栈&#xff1a; 前后端分离的项目 后端&#xff1a;Springboot MybatisPlus 前端&#xff1a;Vue ElementUI 数据库&#xff1a; …

Java 关键字之 native 详解

本篇博客我们将介绍Java中的一个关键字——native。 native 关键字在 JDK 源码中很多类中都有&#xff0c;在 Object.java类中&#xff0c;其 getClass() 方法、hashCode()方法、clone() 方法等等都是用 native 关键字修饰的。 那么为什么要用 native 来修饰方法&#xff0c;…

离线安装MeterSphere遇到的问题

1.安装步骤&#xff0c;参考官方文档 在线安装 - MeterSphere 文档 2.安装完成以后&#xff0c;docker ps查看有很多服务一直处于重启状态&#xff0c;查看容器日志docker logs ID,发现所有一直处于重启状态的容器都是因为服务无法创建日志目录和文件。一直处于重启的服务如…

【Java基础题型】矩阵的对角线之和

二维数组真是存矩阵的好东西啊&#xff0c;现在问题来了&#xff0c;输入一个5*5一共25个数字&#xff0c;要求你求出它们两个对角线上的数字之和&#xff01; 输入格式 25个数字&#xff0c;5行5列 输出格式 它们两个对角线上的和 左上->右下第一条 右上->左下第二…

【秋招笔试】24-07-27-OPPO-秋招笔试题(后端卷)

🍭 大家好这里是清隆学长 ,一枚热爱算法的程序员 💻 ACM金牌团队🏅️ | 多次AK大厂笔试 | 编程一对一辅导 ✨ 本系列打算持续跟新 秋招笔试题 👏 感谢大家的订阅➕ 和 喜欢💗 ✨ 笔试合集传送们 -> 🧷春秋招笔试合集 💡 01.二进制反转游戏 问题描述 K小姐…

Nvidia黄仁勋对话Meta扎克伯格:AI和下一代计算平台的未来 | SIGGRAPH 2024对谈回顾

在今年的SIGGRAPH图形大会上&#xff0c;Nvidia创始人兼CEO黄仁勋与Meta创始人马克扎克伯格进行了一场长达60分钟的对谈。这场对话不仅讨论了AI的未来发展和Meta的开源哲学&#xff0c;还发布了不少新产品&#xff0c;并深入探讨了下一代计算平台的可能性。 引言 人工智能的发…

InternLM-L0 Git作业

0. 任务背景 GitInternStudio 任务说明 GitInternStudio 关卡说明 任务1: 破冰活动&#xff1a;自我介绍 任务2: 实践项目&#xff1a;构建个人项目 1. 破冰活动并构建个人branch Fork 目标项目 git clone https://github.com/sd258sos/Tutorial.git # 修改为自己frok的仓…

模拟实现strcmp,判断二个字符串是否相等

1.判断二个字符串是否相等&#xff0c;可以模仿strcmp.当二个字符串相等的时候ruturn 0.,当二个字符串小于时返回为小于0&#xff0c;当二个字符串大于时返回为大于0。const为不可以更改。 //方法一 int my_strcmp(const char* arr1, const char* arr2) {assert(arr1 &&…

Java后端每日面试题(day6)

目录 创建线程的方式锁的分类公平锁与非公平锁共享式与独占式锁悲观锁与乐观锁 rbac相关的表spring security原理DelegatingFilterProxyFilterChainProxySecurityFilterChain<font colorred>Spring Security 作用机制大致如下&#xff1a; mybatis一对一&#xff0c;一对…

AttributeError: module ‘jwt‘ has no attribute ‘decode‘解决方案

大家好,我是爱编程的喵喵。双985硕士毕业,现担任全栈工程师一职,热衷于将数据思维应用到工作与生活中。从事机器学习以及相关的前后端开发工作。曾在阿里云、科大讯飞、CCF等比赛获得多次Top名次。现为CSDN博客专家、人工智能领域优质创作者。喜欢通过博客创作的方式对所学的…

【大数据】什么是数据集市?如何创建数据集市?

目录 一、数据集市的定义 二、数据集市的存在方式 三、数据集市的特点 四、数据集市和数仓的关系 五、数据集市建设步骤 一、数据集市的定义 数据仓库中集成后的数据&#xff0c;又按照了主题进行了划分&#xff0c;而面向主题划分出来的部分就是数据集市&#xff0c;也就是说数…

织物图像的配准和拼接算法的MATLAB仿真,对比SIFT,SURF以及KAZE

目录 1.算法运行效果图预览 2.算法运行软件版本 3.部分核心程序 4.算法理论概述 4.1 SIFT (Scale-Invariant Feature Transform) 4.2 SURF (Speeded Up Robust Features) 4.3 KAZE (Key-point Affine-Zernike descriptors) 5.算法完整程序工程 1.算法运行效果图预览 (…

python家教预约管理系统 python+django+vue开发的家教信息管理系统 - 毕业设计 - 课程设计

python家教预约管理系统 pythondjangovue开发的家教信息管理系统 - 毕业设计 - 课程设计 文章目录 python家教预约管理系统 pythondjangovue开发的家教信息管理系统 - 毕业设计 - 课程设计一、功能介绍二、代码结构三、运行步骤1、后端运行步骤3、前端运行步骤 四、源程序下载 …

69.搭建分析工具界面

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; 内容参考于&#xff1a;易道云信息技术研究院 上一个内容&#xff1a;68.游戏分析工具设计以及更改辅助中存在的界面问题 以 68.游戏分析工具设计以及更改辅助…

【Dash】简单的直方图

一、Visualizing Data The Plotly graphing library has more than 50 chart types to choose from. In this example, we will make use of the histogram chart. # Import packages from dash import Dash, html, dash_table, dcc import pandas as pd import plotly.expre…

ASUS/华硕幻14 2022 GA402R系列 原厂win11系统 工厂文件 带F12 ASUS Recovery恢复

华硕工厂文件恢复系统 &#xff0c;安装结束后带隐藏分区&#xff0c;一键恢复&#xff0c;以及机器所有驱动软件。 系统版本&#xff1a;windows11 原厂系统下载网址&#xff1a;http://www.bioxt.cn 需准备一个20G以上u盘进行恢复 请注意&#xff1a;仅支持以上型号专用…