函数式编程在 Go 泛型下的实用性探索

发表于 2年以前  | 总阅读数:319 次

背景

函数式编程(Functional Programming / FP)作为一种编程范式,具有无状态、无副作用、并发友好、抽象程度高等优点。目前流行的编程语言(C++、Python、Rust)都或多或少地引入了函数式特性,但在同作为流行语言的 Golang 中却少有讨论。

究其原因,大部分的抱怨Golang 函数式编程简述[1] 、GopherCon 2020: Dylan Meeus - Functional Programming with Go[2]集中于 Go 缺乏对泛型的支持,难以写出类型间通用的函数。代码生成只能解决一部分已知类型的处理,且无法应对类型组合导致复杂度(比如实现一个通用的 TypeA → TypeB 的 map 函数)。

有关泛型的提案 spec: add generic programming using type parameters #43651[3] 已经被 Go 团队接受,并计划在 2022 年初发布支持泛型的 Go 1.18,现在 golang/go 仓库的 master 分支已经支持泛型。

This design has been proposed and accepted as a future language change. We currently expect that this change will be available in the Go 1.18 release in early 2022. Type Parameters Proposal[4]

基于这个重大特性,我们有理由重新看看,函数式特性在 Go 泛型的加持下,能否变得比以往更加实用。

概述

这篇文章里,我们会尝试用 Go 的泛型循序渐进地实现一些常见的函数式特性,从而探索 Go 泛型的优势和不足。

除非额外说明(例如注释中的 // INVALID CODE!!!),文章里的代码都是可以运行的(为了缩减篇幅,部分删去了 package main 声明和 main 函数,请自行添加)。你可以自行 从源码编译[5] 一个 master 版本的 go 来提前体验 Go 的泛型,或者用 The go2go Playground[6] 提供的在线编译器运行单个文件。

泛型语法

提案的 #Very high level overview[7] 一节中描述了为泛型而添加的新语法,这里简单描述一下阅读本文所需要的语法:

  • 函数名后可以附带一个方括号,包含了该函数涉及的类型参数(Type Paramters)的列表:func F[T any](p T "T any") { ... }

  • 这些类型参数可以在函数参数和函数体中(作为类型)被使用

  • 自定义类型也可以有类型参数列表:type M[T any] []T

  • 每个类型参数对应一个类型约束,上述的 any 就是预定义的匹配任意类型的约束

  • 类型约束在语法上以 interface 的形式存在,在 interface 中嵌入类型 T 可以表示这个类型必须是 T:

    type Integer1 interface {
      int
    }
  • 嵌入单个类型意义不大,我们可以用 | 来描述类型的 union:

    type Integer2 interface {
      int | int8 | int16 | int32 | int64
    }
  • ~T 语法可以表示该类型的「基础类型」是 T,比如说我们的自定义类型 type MyInt int 不满足上述的 Integer1 约束,但满足以下的约束:

    type Integer3 interface {
      ~int
    }

提示

「基础类型」在提案中为 “underlying type”,目前尚无权威翻译,在本文中使用仅为方便描述。

高阶函数

在函数式编程语言中, 高阶函数[8] (Higher-order function)是一个重要的特性。高阶函数是至少满足下列一个条件的函数:

  • 接受一个或多个函数作为输入
  • 输出一个函数

Golang 支持闭包,所以实现高阶函数毫无问题:

func foo(bar func() string) func() string {
        return func() string {
                return "foo" + " " + bar()
        }
}

func main() {
        bar := func() string {
                return "bar"
        }
        foobar := foo(bar)
        fmt.Println(foobar())
}
// Output:
// foo bar

filter 操作是高阶函数的经典应用,它接受一个函数 f(func (T) bool)和一个线性表 l([] T),对 l 中的每个元素应用函数 f,如结果为 true,则将该元素加入新的线性表里,否则丢弃该元素,最后返回新的线性表。

根据上面的泛型语法,我们可以很容易地写出一个简单的 filter 函数:

func Filter[T any](f func(T "T any") bool, src []T) []T {
        var dst []T
        for _, v := range src {
                if f(v) {
                        dst = append(dst, v)
                }
        }
        return dst
}

func main() {
        src := []int{-2, -1, -0, 1, 2}
        dst := Filter(func(v int) bool { return v >= 0 }, src)
        fmt.Println(dst)
}
// Output:
// [0 1 2]

代码生成之困

在 1.17 或者更早前的 Go 版本中,要实现通用的 Filter 函数有两种方式:

  1. 使用 interface{} 配合反射,牺牲一定程度的类型安全和运行效率
  2. 为不同数据类型实现不同的 Filter 变种,例如 FilterIntFilterString 等,缺点在于冗余度高,维护难度大

方式 2 的缺点可以通过代码生成规避,具体来说就使用相同的一份模版,以数据类型为变量生成不同的实现。我们在 Golang 内部可以看到不少 代码生成的例子[9] 。

那么,有了代码生成,我们是不是就不需要泛型了呢?

答案是否定的:

1 . 代码生成只能针对已知的类型生成代码,明明这份模版对 float64 也有效,但作者只生成了处理 int 的版本,我们作为用户无能为力(用 interface{} 同理,我们能使用什么类型,取决于作者列出了多少个 type switch 的 cases)

而在泛型里,新的类型约束语法可以统一地处理「基础类型」相同的所有类型:

  type signed interface {
        ~int | ~int8 | ~int16 | ~int32 | ~int64 | ~float32 | ~float64 | ~complex64 | ~complex128
}

func Neg[T signed](n T "T signed") T {
        return -n
}

func main() {
        type MyInt int

        fmt.Println(Neg(1))
        fmt.Println(Neg(1.1))
        fmt.Println(Neg(MyInt(1)))
}
// Output:
// -1
// -1.1
// -1

2 . 代码生成难以应对需要类型组合的场景,我们来看另一个高阶函数 map:接受一个函数 f(func (T1) T2)和一个线性表 l1([]T1),对 l1 中的每个元素应用函数 f,返回的结果组成新的线性表 l2([]T2

如果使用代码生成的话,为了避免命名冲突,我们不得不写出 MapIntIntMapIntUintMapIntString 这样的奇怪名字,而且由于类型的组合,代码生成的量将大大膨胀。

我们可以发现在现有的支持 FP 特性的 Go library 里:

如果使用泛型的话,只需要定义这样的签名就好了:

 func Map[T1, T2 any](f func(T1 "T1, T2 any") T2, src []T1) []T2
  • 有的( hasgo[10] )选择将 map 实现成了闭合运算([]T → []T),牺牲了表达能力
  • 有的( functional-go[11] )强行用代码生成导致接口数目爆炸
  • 有的( fpGo[12] )选择牺牲类型安全用 interface{} 实现

无糖的泛型

Go 的语法在一众的编程语言里绝对算不上简洁优雅。在官网上看到操作 channel 时 <- 的直观便捷让你心下暗喜,而一旦你开始写 real world 的代码,这个语言就处处难掩设计上的简陋。泛型即将到来,而这个语言的其他部分似乎没有做好准备:

闭包语法

在 Haskell 中的匿名函数形式非常简洁:

filter (\x -> x >= 0) [-2, -1, 0, 1, 2]
-- Output:
-- [0,1,2]

而在 Golang 里,函数的类型签名不可省略,无论高阶函数要求何种签名,调用者在构造闭包的时候总是要完完整整地将其照抄一遍Golang 函数式编程简述[13]


func foo(bar func(a int, b float64, c string) string) func() string {
        return func() string {
                return bar(1, 1.0, "")
        }
}

func main() {
        foobar := foo(func(_ int, _ float64, c string) string {
                return c
        })
        foobar()
}

这个问题可以归结于 Go 团队为了保持所谓的「大道至简」,而对类型推导这样提升效率降低冗余的特性的忽视(泛型的姗姗来迟又何尝不是如此呢?)。proposal: Go 2: Lightweight anonymous function syntax #21498[14] 提出了一个简化闭包调用语法的提案,但即使该提案被 accept,我们最快也只能在 Go 2 里见到它了。

方法类型参数

链式调用[15] (Method chaining)是一种调用函数的语法,每个调用都会返回一个对象,紧接着又可以调用该对象关联的方法,该方法同样也返回一个对象。链式调用能显著地消除调用的嵌套,可读性好。我们熟悉的 GORM 的 API 里就大量使用了链式调用:

db.Where("name = ?", "jinzhu").Where("age = ?", 18).First(&user)

在函数式编程中,每个高阶函数往往只实现了简单的功能,通过它们的组合实现复杂的数据操纵。

在无法使用链式调用的情况下,高阶函数的互相组合是这样子的(这仅仅是两层的嵌套):

Map(func(v int) int { return v + 1 },
   Filter(func(v int) bool { return v >= 0 },
      []int{-2, -1, -0, 1, 2}))

如果用链式调用呢?我们继续沿用前面的 filter ,改成以下形式:

type List[T any] []T

func (l List[T]) Filter(f func(T) bool) List[T] {
        var dst []T
        for _, v := range l {
                if f(v) {
                        dst = append(dst, v)
                }
        }
        return List[T](dst "T")
}

func main() {
        l := List[int]([]int{-2, -1, -0, 1, 2} "int").
                Filter(func(v int) bool { return v >= 0 }).
                Filter(func(v int) bool { return v < 2 })
        fmt.Println(l)
}
// Output:
// [0 1]

看起来很美好,但为什么不用 map 操作举例呢?我们很容易写出这样的方法签名:

// INVALID CODE!!!
func (l List[T1]) Map[T2 any](f func(T1 "T1]) Map[T2 any") T2) List[T2]

很遗憾这样的代码是没法通过编译的,我们会获得以下错误:

invalid AST: method must have no type parameter

提案的 #No parameterized methods[16] 一节明确表示了方法(method,也就是有 recevier 的函数)不支持单独指定类型参数:

This design does not permit methods to declare type parameters that are specific to the method. The receiver may have type parameters, but the method may not add any type parameters. 1[17]

这个决定实际上是个不得已的妥协。假设我们实现了上述的方法,就意味对于一个已经实例化了的 List[T] 对象(比如说 List[int]),它的 Map 方法可能有多个版本:Map(func (int) int) List[int] 或者 Map(func (int) string) List[string],当用户的代码调用它们时,它们的代码必然在之前的某个时刻生成了,那么应该在什么时候呢?

  1. 在编译期,更准确地说,在编译的 link 阶段,这需要 linker 去遍历整个 call graph,确定程序中到底使用了几个版本的 Map。问题在于反射(reflection)的存在:用户可以用 reflect.MethodByName 动态地调用对象的方法,所以即使遍历了整个 call graph,我们也无法确保用户的代码到底调用了几个版本的 Map
  2. 在运行期,在第一次调用方法时 yield 到 runtime 中,生成对应版本的函数后 resume 回去,这要求 runtime 支持 JIT(Just-in-time compilation),而目前 Go 并不支持,即使未来 JIT 的支持提上日程,这也不是一蹴而就的事情

综上,Go 团队选择了不支持给 method 指定类型参数,完美了解决这个问题 。

惰性求值

惰性求值[18] (Lazy Evaluation)是另一个重要的函数式特性,一个不严谨的描述是:在定义运算时候,计算不会发生,直到我们需要这个值的时候才进行。其优点在于能使计算在空间复杂度上得到极大的优化。

下面的代码展示了一个平平无奇的 Add 函数和它的 Lazy 版本,后者在给出加数的时候不会立刻计算,而是返回一个闭包:

func Add(a, b int) int {
        return a + b
}

func LazyAdd(a, b int) func() int {
        return func () int {
                return a + b
        }
}

上面这个例子没有体现出惰性求值节省空间的优点。基于我们之前实现的高阶函数,做以下的运算:

l := []int{-2, -1, -0, 1, 2}
l = Filter(func(v int) bool { return v > -2 }, l)
l = Filter(func(v int) bool { return v < 2 }, l)
l = Filter(func(v int) bool { return v != 0 }, l)
fmt.Println(l)

计算过程中会产生 3 个新的长度为 5 的 []int,空间复杂度为 O(3∗N),尽管常数在复杂度分析时经常被省略,但在程序实际运行的时候,这里的 3 就意味着 3 倍的内存占用。

假设这些高阶函数的求值是惰性的,则计算只会在对 fmt.Println 对参数求值的时候发生,元素从原始的 l 中被取出,判断 if v > -2if v < 2,最后执行 v + 1,放入新的 []int 中,空间复杂度依然是 O(N),但毫无疑问地我们只使用了一个 `[]int``。

泛型的引入对惰性求值的好处有限,大致和前文所述一致,但至少我们可以定义类型通用的 接口了:

// 一个适用于线性结构的迭代器接口
type Iter[T any] interface{ Next() (T, bool) }

// 用于将任意 slice 包装成 Iter[T]
type SliceIter[T any] struct {
        i int
        s []T
}

func IterOfSlice[T any](s []T "T any") Iter[T] {
        return &SliceIter[T]{s: s}
}

func (i *SliceIter[T]) Next() (v T, ok bool) {
        if ok = i.i < len(i.s); ok {
                v = i.s[i.i]
                i.i++
        }
        return
}

接着实现惰性版本的 filter:

type filterIter[T any] struct {
        f   func(T) bool
        src Iter[T]
}

func (i *filterIter[T]) Next() (v T, ok bool) {
        for {
                v, ok = i.src.Next()
                if !ok || i.f(v) {
                        return
                }
        }
}

func Filter[T any](f func(T "T any") bool, src Iter[T]) Iter[T] {
        return &filterIter[T]{f: f, src: src}
}

可以看到这个版本的 filter 仅仅返回了一个 Iter[T]*filterIter[T]),实际的运算在 *filterIter[T].Next() 中进行。

我们还需要一个将 Iter[T] 转回 []T 的函数:

func List[T any](src Iter[T] "T any") (dst []T) {
        for {
                v, ok := src.Next()
                if !ok {
                        return
                }
                dst = append(dst, v)
        }
}

最后实现一个和上面等价的运算,但实际的计算工作是在 List(i) 的调用中发生的:

i := IterOfSlice([]int{-2, -1, -0, 1, 2})
i = Filter(func(v int) bool { return v > -2 }, i)
i = Filter(func(v int) bool { return v < 2 }, i)
i = Filter(func(v int) bool { return v != 0 }, i)
fmt.Println(List(i))

Map 的迭代器

Golang 中的 Hashmap map[K]V 和 Slice []T 一样是常用的数据结构,如果我们能将 map 转化为上述的 Iter[T],那么 map 就能直接使用已经实现的各种高阶函数。

map[K]V 的迭代只能通过 for ... range 进行,我们无法通过常规的手段获得一个 iterator。反射当然可以做到,但 reflect.MapIter 太重了。modern-go/reflect2[19] 提供了一个 更快的实现[20] ,但已经超出了本文的讨论范围,此处不展开,有兴趣的朋友可以自行研究。

局部应用

局部应用[21] (Partial Application)是一种固定多参函数的部分参数,并返回一个可以接受剩余部分参数的函数的操作。

备注

局部应用不同于 柯里化[22] (Currying) Partial Function Application is not Currying[23],柯里化是一种用多个单参函数来表示多参函数的技术,在 Go 已经支持多参函数的情况下,本文暂时不讨论 Currying 的实现。

我们定义一个有返回值的接收单个参数的函数类型:

type FuncWith1Args[A, R any] func(A) R

对一个只接受一个参数的函数进行一次 partial application,其实就相当于求值:

func (f FuncWith1Args[A, R]) Partial(a A) R {
        return f(a)
}

接受两个参数的函数被 partial application 后,一个参数被固定,自然返回一个上述的 FuncWith1Args:

type FuncWith2Args[A1, A2, R any] func(A1, A2) R

func (f FuncWith2Args[A1, A2, R]) Partial(a1 A1) FuncWith1Args[A2, R] {
        return func(a2 A2) R {
                return f(a1, a2)
        }
}

我们来试用一下,将我们之前实现的 filter 包装成一个 FuncWith2Args,从左到右固定两个参数,最后得到结果:

f2 := FuncWith2Args[func(int) bool, Iter[int], Iter[int]](Filter[int] "func(int) bool, Iter[int], Iter[int]")
f1 := f2.Partial(func(v int) bool { return v > -2 })
r := f1.Partial(IterOfSlice([]int{-2, -1, -0, 1, 2}))
fmt.Println(List(r))
// Output:
// [-1 0 1 2]

类型参数推导

我们勉强实现了 partial application,可是把 Filter 转换为 FuncWith2Args 的过程太过繁琐,在上面的例子中,我们把类型参数完整地指定了一遍,是不是重新感受到了 闭包语法[24] 带给你的无奈?

这一次我们并非无能为力,提案中的 #Type inference[25] 一节描述了对类型参数推导的支持情况。上例的转换毫无歧义,那我们把类型参数去掉:

// INVALID CODE!!!
f2 := FuncWith2Args(Filter[int])

编译器如是抱怨:

cannot use generic type FuncWith2Args without instantiation

提案里的类型参数推导仅针对函数调用,FuncWith2Args(XXX) 虽然看起来像是函数调用语法,但其实是一个类型的实例化,针对类型实例化的参数类型推导( #Type inference for composite literals[26] )还是一个待定的 feature。

如果我们写一个函数来实例化这个对象呢?很遗憾,做不到:我们用什么表示入参呢?只能写出这样「听君一席话,如听一席话」的函数:

func Cast[A1, A2, R any](f FuncWith2Args[A1, A2, R] "A1, A2, R any") FuncWith2Args[A1, A2, R] {
        return f
}

但是它能工作!当我们直接传入 Filter 的时候,编译器会帮我们隐式地转换成一个 FuncWith2Args[func(int) bool, Iter[int], Iter[int]]!同时因为函数类型参数推导的存在,我们不需要指定任何的类型参数了:

f2 := Cast(Filter[int])
f1 := f2.Partial(func(v int) bool { return v > -2 })
r := f1.Partial(IterOfSlice([]int{-2, -1, -0, 1, 2}))
fmt.Println(List(r))
// Output:
// [-1 0 1 2]

可变类型参数

FuncWith1ArgsFuncWith2Args 这些名字让我们有些恍惚,仿佛回到了代码生成的时代。为了处理更多的参数,我们还得写 FuncWith3ArgsFuncWith4Args… 吗?

是的, #Omissions[27] 一节提到:Go 的泛型不支持可变数目的类型参数:

No variadic type parameters. There is no support for variadic type parameters, which would permit writing a single generic function that takes different numbers of both type parameters and regular parameters.

对应到函数签名,我们也没有语法来声明拥有不同类型的可变参数。

类型系统

众多函数式特性的实现依赖于一个强大类型系统,Go 的类型系统显然不足以胜任,作者不是专业人士,这里我们不讨论其他语言里让人羡慕的类型类(Type Class)、代数数据类型(Algebraic Data Type),只讨论在 Go 语言中引入泛型之后,我们的类型系统有哪些水土不服的地方。

提示

其实上文的大部分问题都和类型系统息息相关,case by case 的话我们可以列出非常多的问题,因此以下只展示明显不合理那部分。

编译期类型判断

当我们在写一段泛型代码里的时候,有时候会需要根据 T 实际上的类型决定接下来的流程,可 Go 的完全没有提供在编译期操作类型的能力。运行期的 workaround 当然有,怎么做呢:将 T 转化为 interface{},然后做一次 type assertion:

func Foo[T any](n T "T any") {
        if _, ok := (interface{})(n).(int); ok {
                // do sth...
        }
}

无法辨认「基础类型」

我们在 代码生成之困[28] 提到过,在类型约束中可以用 ~T 的语法约束所有 基础类型为 T 的类型,这是 Go 在语法层面上首次暴露出「基础类型」的概念,在之前我们只能通过 reflect.(Value).Kind 获取。而在 type assertion 和 type switch 里并没有对应的语法处理「基础类型」:

type Int interface {
        ~int | ~uint
}

func IsSigned[T Int](n T "T Int") {
        switch (interface{})(n).(type) {
        case int:
                fmt.Println("signed")
        default:
                fmt.Println("unsigned")
        }
}

func main() {
        type MyInt int
        IsSigned(1)
        IsSigned(MyInt(1))
}
// Output:
// signed
// unsigned

乍一看很合理,MyInt 确实不是 int。那我们要如何在函数不了解 MyInt 的情况下把它当 int 处理呢?答案是还不能:#Identifying the matched predeclared type[29] 表示这是个未决的问题,需要在后续的版本中讨论新语法。总之,在 1.18 中,我们是见不到它了。

类型约束不可用于 type assertion

一个直观的想法是单独定义一个 Signed 约束,然后判断 T 是否满足 Signed:

type Signed interface {
        ~int
}

func IsSigned[T Int](n T "T Int") {
        if _, ok := (interface{})(n).(Signed); ok {
                fmt.Println("signed")
        } else {
                fmt.Println("unsigned")
        }
}

但很可惜,类型约束不能用于 type assertion/switch,编译器报错如下:

interface contains type constraints

尽管让类型约束用于 type assertion 可能会引入额外的问题,但牺牲这个支持让 Go 的类型表达能力大大地打了折扣。

总结

函数式编程的特性不止于此,代数数据类型、引用透明(Referential Transparency)等在本文中都未能覆盖到。总得来说,Go 泛型的引入:

  1. 使的部分 函数式特性能以更通用的方式被实现
  2. 灵活度比代码生成更高 ,用法更自然,但细节上的小问题很多
  3. 1.18 的泛型在引入 type paramters 语法之外并没有其他大刀阔斧的改变,导致泛型和这个语言的其他部分显得有些格格不入,也使得泛型的能力受限。至少在 1.18 里,我们要忍受泛型中存在的种种不一致
  4. 受制于 Go 类型系统的表达能力,我们无法表示复杂的类型约束,自然也 无法实现完备的函数式特性

参考资料

[1]Golang 函数式编程简述: https://hedzr.com/golang/fp/golang-functional-programming-in-brief/

[2]GopherCon 2020: Dylan Meeus - Functional Programming with Go: https://www.youtube.com/watch?v=wqs8n5Uk5OM

[3]spec: add generic programming using type parameters #43651: https://github.com/golang/go/issues/43651

[4]Type Parameters Proposal: https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md

[5]从源码编译: https://golang.org/doc/install/source#install

[6]The go2go Playground: https://go2goplay.golang.org/

[7]#Very high level overview: https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md#very-high-level-overview

[8] 高阶函数: https://zh.wikipedia.org/wiki/高阶函数

[9]代码生成的例子: https://github.com/golang/go/search?q=filename%3Agen.go

[10]hasgo: https://pkg.go.dev/github.com/DylanMeeus/hasgo/types?utm_source=godoc#Ints.Map

[11]functional-go: https://pkg.go.dev/github.com/logic-building/functional-go/fp

[12]fpGo: https://pkg.go.dev/github.com/TeaEntityLab/fpGo#Map

[13]Golang 函数式编程简述: https://hedzr.com/golang/fp/golang-functional-programming-in-brief/

[14]proposal: Go 2: Lightweight anonymous function syntax #21498: https://github.com/golang/go/issues/21498

[15]链式调用: https://wikipedia.org/wiki/Method_chaining

[16]#No parameterized methods: https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md#No-parameterized-methods

[17]1: https://silverrainz.me/blog/funtional-programming-in-go-generics.html#id39

[18] 惰性求值: https://zh.wikipedia.org/wiki/惰性求值

[19]K]V的迭代只能通过for ... range进行,我们无法通过常规的手段获得一个 iterator。反射当然可以做到,但reflect.MapIter` 太重了。[modern-go/reflect2: https://github.com/modern-go/reflect2

[20]更快的实现: https://pkg.go.dev/github.com/modern-go/reflect2#UnsafeMapIterator

[21]局部应用: https://wikipedia.org/wiki/Partial_application

[22] 柯里化: https://zh.wikipedia.org/wiki/柯里化

[23]Partial Function Application is not Currying: https://www.uncarved.com/articles/not-currying/

[24]闭包语法: https://silverrainz.me/blog/funtional-programming-in-go-generics.html#id18

[25]#Type inference: https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md#type-inference

[26]#Type inference for composite literals: https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md#type-inference-for-composite-literals

[27]#Omissions: https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md#omissions

[28]代码生成之困: https://silverrainz.me/blog/funtional-programming-in-go-generics.html#id12

[29]#Identifying the matched predeclared type: https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md#identifying-the-matched-predeclared-type

本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/wPLH4agJr-LozUsOqwN08g

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237269次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8108次阅读
 目录