错误: 至少有一个需要的隐性或转发依赖函数没找到。_【翻译】自动柯里化Rust函数...

2023-11-12

原文标题: Auto-currying Rust Functions

原文链接: https://peppe.rs/posts/auto-currying_rust_functions/

公众号: Rust碎碎念

本文包含Rust中过程宏(procedural macros)的介绍示例和通过过程宏柯里化Rust函数的指导。 整个库的源码可以在这里[1]找到。 在crate.io上也可以找到。
在开始之前,阅读下面的链接会有助于理解:

  1. 过程宏(Procedural Macros)[2]
  2. 柯里化(Currying)[3]

或者你也可以假装已经阅读了上面的文章,因为我这里已经包含了基本介绍。

内容

  1. 柯里化(Currying)
  2. 过程宏
  3. 定义
  4. 改进
  5. 插曲
  • 5.1 依赖
  • 5.2 属性宏
  • 5.3 函数体
  • 5.4 函数签名
  • 5.5
调试和测试注意事项总结

柯里化(Currying)

柯里化是指把形如f(a, b, c)的函数转换为 f(a)(b)(c)的过程。一个被柯里化的函数只有在它接收到所有的参数之后才会返回一个具体的值!如果它没有接受到足够数量的参数,比如3个参数中的1个,它仍然返回一个柯里化的函数,该函数会在收到剩余2个参数之后返回。

curry(f(a, b, c)) = h(a)(b)(c)

h(x) = g g(y) = k k(z) = v
Keen readers will conclude the following,
h(x)(y)(z) = g(y)(z) = k(z) = v

从数学上来讲,如果f是一个接收参数为xy的函数, 使x ∈ X, y ∈ Y :

f: (X × Y) -> Z

其中 × 表示集合X和集合Y进行笛卡尔乘积,柯里化的f(这里标记为h)表示为:

h: X -> (Y -> Z)

过程宏(Procedural Macros)

它以代码作为输入,以修改后的代码作为输出。Rust有三种过程宏:

  1. 函数风格的宏
  2. 继承宏: #[derive(...)], 常被用于为结构体/枚举自动地实现trait
  3. 属性宏: #[test], 通常附着在函数上

我们将会使用属性宏来对Rust函数进行柯里化,即变成通过function(arg1)(arg2)这种方式来调用的函数。

定义(Definitions)

作为受人尊敬的程序员,我们定义了输入以及过程宏的输出。我们从一个比较特别的函数开始:

fn add(x: u32, y: u32, z: u32) -> u32 {
  return x + y + z;
}

我们的输出会是什么样子?理想情况下我们的过程宏应该生成什么?如果我们正确理解了柯里化,我们应该接收一个参数并且返回一个接收一个参数的函数,然后这个函数返回一个...你应该懂了,就像下面这样:

fn add_curried1(x: u32) -> ? {
  return fn add_curried2 (y: u32) -> ? {
    return fn add_curried3 (z: u32) -> u32 {
      return x + y + z;
    }
  }
}

有几件事需要注意:

返回类型(Return types)

我们用?替代了返回类型。让我们尝试修正它。add_curried3返回了一个值,所以这里返回u32是准确的。add_curried2返回了add_curried3, 所以add_curried3的类型是什么?它是一个函数,以u32作为参数并返回一个u32, 所以fn(u32)->u32正确么?不, 接下来我将会解释原因,但是现在,我们将会利用Fntrait, 让我们的返回类型impl Fn(u32)->u32。这基本上是在告诉编译器我们将会返回类似函数的东西, 行为就像Fn, 酷!
如果你已经跟上来了,你应该就能猜到add_curried1的返回类型是

impl Fn(u32) -> (impl Fn(u32) -> u32)

我们可以丢掉外面的括号,应该->是右关联的:

impl Fn(u32) -> impl Fn(u32) -> u32

访问上下文环境(Accessing environment)

函数是不能访问周围上下文环境的。我们的方案无法生效。add_curried3尝试访问x,这是不被允许的! 但是闭包[4]可以。如果我们返回一个闭包,我们返回类型就必须impl Fn而不是fnFntrait和函数指针的区别已经超出本文范围,不再讨论。

改进(Refinement)

掌握了上述知识,我们改进期望的输出去,这一次,我们使用闭包:

fn add(x: u32) -> impl Fn(u32) -> impl Fn(u32) -> u32 {
  return move |y| move |z| x + y + z;
}

但是这样无法编译通过,编译器报出下面的错误信息:

error[E0562]: `impl Trait` not allowed outside of function
and inherent method return types
--> src/main.rs:17:37
|
| fn add(x: u32) -> impl Fn(u32) -> impl Fn(u32) -> u32
|

只能在函数内部返回一个impl Fn。我们现在是从另一个返回里返回。至少,这是我从错误信息里获得的最多的理解。
我们不得不通过欺骗编译器来修正这个问题;通过使用类型别名(type aliase)和nightly版本中的一个特性[5]

#![feature(type_alias_impl_trait)]  // allows us to use `impl Fn` in type aliases!

type T0 = u32;                 // the return value when zero args are to be applied
type T1 = impl Fn(u32) -> T0;  // the return value when one arg is to be applied
type T2 = impl Fn(u32) -> T1;  // the return value when two args are to be applied

fn add(x: u32) -> T2 {
  return move |y| move |z| x + y + z;
}

把上面的代码丢到cargo工程里,然后调用add(4)(5)(6), 祈求成功,然后运行cargo +nightly run。你应该看到一个15,除非你忘记打印它。

插曲(The In-Betweens)

让我们写一些能够把函数进行柯里化的神奇的片段。使用cargo new --lib currying初始化你的工作空间。过程宏crates是只把自身进行导出的库。在crate根目录下添加一个tests目录。你的目录看起来应该像下面这样:

.
├── Cargo.toml
├── src
│   └── lib.rs
└── tests
    └── smoke.rs

依赖(Dependencies)

我们总共将会使用3个外部的crate:

  1. proc_macro2[6]
  2. syn[7]
  3. quote[8]

这里是Cargo.toml的示例:

# Cargo.toml

[dependencies]
proc-macro2 = "1.0.9"
quote = "1.0"

[dependencies.syn]
version = "1.0"
features = ["full"]

[lib]
proc-macro = true # this is important!

我们将会使用一个外部的proc-macro2 create, 和一个内部的proc-macrocrate。对此无需困惑。

属性宏(The attribute macro)

把下面这些丢进src/lib.rs, 准备开始。

// src/lib.rs

use proc_macro::TokenStream;  // 1
use quote::quote;
use syn::{parse_macro_input, ItemFn};

#[proc_macro_attribute]   // 2
pub fn curry(_attr: TokenStream, item: TokenStream) -> TokenStream {
  let parsed = parse_macro_input!(item as ItemFn);  // 3
  generate_curry(parsed).into()  // 4
}

fn generate_curry(parsed: ItemFn) -> proc_macro2::TokenStream {}

1. 导入(Imports)

一个Tokenstream包含(希望是有效的)Rust代码,这是我们输入和输出的类型。注意,我们是从proc_macro而不是proc_macro2导出这个类型。quotecrate里的quote!是一个能够让我们快速生成TokenStream的宏。和LISP的quote过程很像,你可以使用quote!宏来进行符号转换。syncrate中的ItemFn含有被解析的一个Rust函数里的TokenStreamparse_macro_input!syn提供的一个很有用的宏。

2. 单独导出(The lone export)

使用#[proc_macro_attribute]来标注我们的crate中的pub部分。这告诉rustccurry是一个过程宏,并且我们能够在其他的crate中通过#[crate_name::curry]来使用它。注意curry函数的签名。_attr是代表属性自身的TokenStream,item表示我们要把宏改造成的事物,在这个例子中是个函数(比如add)。返回值是被修改后的TokenStream, 这将会包含我们对add进行柯里化的版本。

3. 辅助宏(The helper macro)

一个TokenStream难以完成工作,这也是为什么我们要有一个能够为Rust符号提供表达类型的syncrate。一个RArrow结构体表示一个函数后面的返回箭头符号,诸如此类。其中一个类型就是ItemFn,它表示一整个Rust函数。parse_macro_input!自动把宏的输入放入ItemFn

4. 返回TokenStreams(Returning TokenStreams)

我们还没有完善generate_curry, 但是我们能看到它返回一个proc_macro2::TokenStream而不是proc_macro::TokenStream, 所以我们使用一个.into()来进行转换。
让我们继续完善generate_curry, 我会建议你打开syn::ItemFn[9]和syn::Signature[10]的文档。

// src/lib.rs

fn generate_curry(parsed: ItemFn) -> proc_macro2::TokenStream {
  let fn_body = parsed.block;      // function body
  let sig = parsed.sig;            // function signature
  let vis = parsed.vis;            // visibility, pub or not
  let fn_name = sig.ident;         // function name/identifier
  let fn_args = sig.inputs;        // comma separated args
  let fn_return_type = sig.output; // return type
}

我们简单地导出这个函数的片段, 我们将会重用原始函数的可见性和名字。来看看syn::Signature可以告诉我们一个函数的什么有关信息:

                       .-- syn::Ident (ident)
                      /
                 fn add(x: u32, y: u32) -> u32
  (fn_token)      /     ~~~~~~~,~~~~~~  ~~~~~~
syn::token::Fn --'            /               \       (output)
                             '                 `- syn::ReturnType
             Punctuated (inputs)

分析得差不多了,让我们写下第一段Rust代码。

函数体(Function Body)

回想一下,柯里化的add函数应该是像下面这样:

return move |y| move |z| x + y + z;

更通用一点:

return move |arg2| move |arg3| ... |argN| 

generate_curry函数里,我们已经有了由fn_body提供的函数体。接下来要做的就是把move |arg2| move |arg3| ...这些东西添加进去,要完成这个目标,我们需要导出参数的标识(文档:Punctuated[11],FnArg[12],PatType[13]):

// src/lib.rs
use syn::punctuated::Punctuated;
use syn::{parse_macro_input, FnArg, Pat, ItemFn, Block};

fn extract_arg_idents(fn_args: Punctuated) -> Vec<Box> { return fn_args.into_iter().map(extract_arg_pat).collect::<Vec<_>>();
}

好吧,所以我们正在遍历函数的参数(Punctuated是一个你可以进行遍历的集合)并且把extract_arg_pat映射到每一项。extract_arg_pat是什么?

// src/lib.rs

fn extract_arg_pat(a: FnArg) -> Box {match a {
    FnArg::Typed(p) => p.pat,
    _ => panic!("Not supported on types with `self`!"),
  }
}

或许你已经猜到,FnArg是一个枚举类型。Typed变量包含一些形式为name:type的参数和其他的变量,Receiver指向self类型。现在先忽略这些,保持简单。
每个FnArg::Typed值包含一个pat,从本质上讲,pat是参数的名字。参数的类型可以通过p.ty来获取(我们会在后面用到)。
了解上述内容后,我们应该能够写出函数体的代码生成:

// src/lib.rs

fn generate_body(fn_args: &[Box], body: Box) -> proc_macro2::TokenStream {
  quote! {return #( move |#fn_args|  )* #body
  }
}

这些语法令人感到恐怖。容我来解释一下。quote!{ ... }返回一个proc_macro2::TokenStream,如果我们写quote!{ let x = 1 + 2; },这不会创建一个值为3的变量x,它只会产生关于这个表达式的一个符号流。#能够插入变量。#body将会在当前域中查找body, 获取它的值, 然后在返回的TokenStream中将其插入。有点像LISP中的准引用。
那么#( move |#fn_args| )*是什么呢?这是重复。quote遍历fn_args,然后在每一个参数前面加上一个move,然后在参数两边放上竖线|
让我们先测试一段代码生成!把generate_curry修改成下面这样:

// src/lib.rs

 fn generate_curry(parsed: ItemFn) -> TokenStream {
   let fn_body = parsed.block;
   let sig = parsed.sig;
   let vis = parsed.vis;
   let fn_name = sig.ident;
   let fn_args = sig.inputs;
   let fn_return_type = sig.output;

+  let arg_idents = extract_arg_idents(fn_args.clone());
+  let first_ident = &arg_idents.first().unwrap();

+  // remember, our curried body starts with the second argument!
+  let curried_body = generate_body(&arg_idents[1..], fn_body.clone());
+  println!("{}", curried_body);

   return TokenStream::new();
 }

tests/目录下加上测试:

// tests/smoke.rs

#[currying::curry]
fn add(x: u32, y: u32, z: u32) -> u32 {
  x + y + z
}

#[test]
fn works() {
  assert!(true);
}

你应该能在cargo test的输出里找到一些类似下面这样的内容:

return move | y | move | z | { x + y + z }

极好的println!调试!

函数签名(Function signature)

这一部分将要深入宏更复杂的部分-生成类型别名和函数签名。到了这部分结尾,我们就能拥有一个可以完整工作的自动柯里化的宏!
回顾一下,对于我们的add函数,生成的类型别名应该看起来是什么样子:

type T0 = u32;
type T1 = impl Fn(u32) -> T0;
type T2 = impl Fn(u32) -> T1;

更通用一点儿:

type T0 = <return type>;
type T1 = impl Fn(<type of arg N>) -> T0;
type T2 = impl Fn(<type of arg N - 1>) -> T1;
.
.
.
type T(N-1) = impl Fn(<type of arg 2>) -> T(N-2);

要生成上面的内容,我们需要下面这些类型:

  1. 所有的输入(参数)
  2. 输出(返回类型)

要获取所有输入的类型,我们可以简单地复用之前写过的获取变量名的代码片段(doc: Type[14])

// src/lib.rs

use syn::{parse_macro_input, Block, FnArg, ItemFn, Pat, ReturnType, Type};

fn extract_type(a: FnArg) -> Box {match a {
    FnArg::Typed(p) => p.ty,  // notice `ty` instead of `pat`
      _ => panic!("Not supported on types with `self`!"),
  }
}fn extract_arg_types(fn_args: Punctuated) -> Vec<Box> {return fn_args.into_iter().map(extract_type).collect::<Vec<_>>();
}

一个好的读者应该已经看过syn::Signature结构体的输出成员的相关文档。它的类型是syn::ReturnType,所以这里是没有提取操作对么?事实上,这里真的有些事是需要我们确认的:

  1. 我们需要确认函数返回值!在这种情况下,一个没有返回的函数是无意义的,我会在最后的注意部分解释原因。
  2. 一个ReturnType会关闭返回的箭头,我们需要避免这种情况。回顾:
type T0 = u32
// and not
type T0 = -> u32

下面是处理导出返回类型的代码段(doc:syn::ReturnType[15]):

// src/lib.rs

fn extract_return_type(a: ReturnType) -> Box {match a {
    ReturnType::Type(_, p) => p,
    _ => panic!("Not supported on functions without return types!"),
  }
}

你或许已经注意到,我们正在广泛使用panic!宏。很好,这是因为当收到一个不满足条件的TokenStream时执行退出是一个很好的主意。
类型已经准备好了,现在我们可以继续生产类型别名:

// src/lib.rs

use quote::{quote, format_ident};

fn generate_type_aliases(
  fn_arg_types: &[Box],
  fn_return_type: Box,
  fn_name: &syn::Ident,
) -> Vec<:tokenstream> {    // 1let type_t0 = format_ident!("_{}_T0", fn_name);    // 2let mut type_aliases = vec![quote! { type #type_t0 = #fn_return_type  }];// 3for (i, t) in (1..).zip(fn_arg_types.into_iter().rev()) {let p = format_ident!("_{}_{}", fn_name, format!("T{}", i - 1));let n = format_ident!("_{}_{}", fn_name, format!("T{}", i));
    type_aliases.push(quote! {type #n = impl Fn(#t) -> #p
    });
  }return type_aliases;
}

1. 返回值

我们正在返回一个Vec<:tokenstream>, 也就是,一个TokenStream列表,其中每一项都一个类型别名。

2. 格式化标识符

我对此有一些解释。显然,我们正在尝试写第一个类型别名,然后用T0初始化我们的TokenStream vector,因为它和其他的不一样。

type T0 = something
// the others are of the form
type Tr = impl Fn(something) -> something

format_ident!format!相似。它返回一个syn::Ident而不是一个格式化后的字符串。因此, type_t0实际上是一个标识符,在我们的例子中,add函数的标识符就是_add_T0。为什么这个格式化很重要?命名空间。
看下面的代码,我们有两个函数,addsubtract, 我们希望通过宏对这两个函数进行柯里化。

#[curry]
fn add(...) -> u32 { ... }

#[curry]
fn sub(...) -> u32 { ... }

下面的功能是一样的,但是对宏进行了展开:

type T0 = u32;
type T1 = impl Fn(u32) -> T0;
fn add( ... ) -> T1 { ... }

type T0 = u32;
type T1 = impl Fn(u32) -> T0;
fn sub( ... ) -> T1 { ... }

我们最终拥有对T0的两个定义。现在,我们进行少量的format_ident!

type _add_T0 = u32;
type _add_T1 = impl Fn(u32) -> _add_T0;
fn add( ... ) -> _add_T1 { ... }

type _sub_T0 = u32;
type _sub_T1 = impl Fn(u32) -> _sub_T0;
fn sub( ... ) -> _sub_T1 { ... }

类型别名不再互相冲突了。记得要从quotecrate里导出format_ident

3. The TokenStream Vector

我们以相反的顺序遍历我们的类型(T0是最后的返回, T1是倒数第二个,以此类推),使用zip对每一次迭代赋一个数字,使用format_ident生成类型别名,使用quote和变量插入把TokenStream送入(vector)。
如果你想知道为什么我们使用(1..).zip()而不是.enumerate(),这是因为我们想要从1开始而不是从0开始(我们已经用过T0了)。

汇总(Getting it together)

我承诺过到最后一部分,我们将会拥有一个能够完成工作的宏。我撒谎了,我们不得不把所有的东西汇总到generate_curry函数里。

// src/lib.rs

 fn generate_curry(parsed: ItemFn) -> proc_macro2::TokenStream {
   let fn_body = parsed.block;
   let sig = parsed.sig;
   let vis = parsed.vis;
   let fn_name = sig.ident;
   let fn_args = sig.inputs;
   let fn_return_type = sig.output;

   let arg_idents = extract_arg_idents(fn_args.clone());
   let first_ident = &arg_idents.first().unwrap();
   let curried_body = generate_body(&arg_idents[1..], fn_body.clone());

+  let arg_types = extract_arg_types(fn_args.clone());
+  let first_type = &arg_types.first().unwrap();
+  let type_aliases = generate_type_aliases(
+      &arg_types[1..],
+      extract_return_type(fn_return_type),
+      &fn_name,
+  );

+  let return_type = format_ident!("_{}_{}", &fn_name, format!("T{}", type_aliases.len() - 1));

+  return quote! {
+      #(#type_aliases);* ;
+      #vis fn #fn_name (#first_ident: #first_type) -> #return_type {
+          #curried_body ;
+      }
+  };
 }

大部分增加的代码都很容易理解,我会和你一起看看返回语句。我们返回一个quote!{ ... },也就是一个proc_macro2::TokenStream。我们正在遍历type_aliases变量,你应该可以想到,这是一个vec。你可能注意到在*前面不起眼的分号。这是在告诉quote, 要插入一项,然后是一个分号,然后插入下一项,然后是下一个分号,以此类推。这个分号是一个分隔符。我们需要手动插入另一个分号在全部结束的时候,quote不会在迭代结束后插入一个分隔符。
我们保持原来函数的可见性和名字。我们的柯里化后的函数接收参数,就像原来的函数接收的第一个参数一样。柯里化函数的返回类型是我们创建的最后的类型别名。如果你回想一下我们手工进行柯里化的add函数,我们返回了T2,事实上T2就是我们创建的最后的类型别名。
我保证,到这里,你一定渴望测试一下输出结果,但是在那之前,让我为你介绍一些调试过程宏代码的好方法。

调试和测试(Debugging and Testing)

通过下面的命令安装cargo-expand:

cargo install cargo-expand

cargo-expand是一个简洁的工具,它能够把你的宏在原地展开,让你看到生成的代码!例如:

# create a bin package hello
$ cargo new hello

# view the expansion of the println! macro
$ cargo expand

#![feature(prelude_import)]
#[prelude_import]
use std::prelude::v1::*;
#[macro_use]
extern crate std;
fn main() {
{
::std::io::_print(::core::fmt::Arguments::new_v1(
&["Hello, world!\n"],
&match () {
() => [],
},
));
};
}

不使用cargo-expand来写过程宏就像驾驶没有后视镜的货车。它给你一双眼睛看到背后发生的事情。
现在,你的宏将不会总是编译,你只要收到蜜蜂电影剧本当做错误(译者注:这句话没看懂)。cargo-expand将不会在这样的情况下工作。我建议打印出你的变量以便于检查。TokenStream实现了DisplayDebug。我们不必总是令人尊重的程序员。只要把它打印出来。
现在让我们开始测试:

// tests/smoke.rs

#![feature(type_alias_impl_trait)]

#[crate_name::curry]
fn add(x: u32, y: u32, z: u32) -> u32 {
   x + y + z
}

#[test]
fn works() {
  assert_eq!(15, add(4)(5)(6));
}

运行cargo +nightly test。你应该能够看到一条令人愉快的消息:

running 1 test
test tests::works ... ok

通过cargo +nightly expand --tests smoke看一看我们的柯里化宏的展开:

type _add_T0 = u32;
type _add_T1 = impl Fn(u32) -> _add_T0;
type _add_T2 = impl Fn(u32) -> _add_T1;
fn add(x: u32) -> _add_T2 {
  return (move |y| {
    move |z| {
      return x + y + z;
    }
  });
}

// a bunch of other stuff generated by #[test] and assert_eq!

这是个更复杂的例子,它生产前十个自然数的十倍数:

#[curry]
fn product(x: u32, y: u32) -> u32 {
  x * y
}

fn multiples() -> Vec<Vec<u32>>{
  let v = (1..=10).map(product);
  return (1..=10)
      .map(|x| v.clone().map(|f| f(x)).collect())
      .collect();
}

注意(Notes)

我没有解释为什么我们在闭包里使用move |arg|。这是因为我们想要获取提供给我们的变量的所有权。看看下面的例子:

let v = add(5);
let g;
{
  let x = 5;
  g = v(x);
}
println!("{}", g(2));

变量xg能够返回一个具体值之前就离开了作用域。如果我们通过把它move到我们的闭包中来获取x的所有权,这就能达到我们预期的工作。事实上,rustc理解这个,并且强制你使用movemove的使用就解释了为什么一个没有返回的柯里化函数是无意义的。我们传递给柯里化函数每个变量都被移动进了它的本地作用域。使用这个变量不会引起作用域之外的变化。返回是我们唯一的和函数体外部进行交互的方式。

总结(Conclusion)

柯里化可能不是总是有用。Rust中的柯里化函数稍显笨重,因为标准库没有围绕柯里化来构建。如果你喜欢柯里化带来的可能性,可以考虑看一下Haskell或者Scheme。
我最初的目的是想写一篇简短的博客,但是现在看来写的有点长了。
或许我应该叫它宏博客:)

参考资料

[1]

这里: https://github.com/nerdypepper/cutlass

[2]

过程宏(Procedural Macros): https://doc.rust-lang.org/reference/procedural-macros.html

[3]

柯里化(Currying): https://en.wikipedia.org/wiki/Currying

[4]

闭包: https://doc.rust-lang.org/book/ch13-01-closures.html

[5]

特性: https://peppe.rs/posts/auto-currying_rust_functions/#fn2

[6]

proc_macro2: https://docs.rs/proc-macro2/1.0.12/proc_macro2/

[7]

syn: https://docs.rs/syn/1.0.18/syn/index.html

[8]

quote: https://docs.rs/quote/1.0.4/quote/index.html

[9]

syn::ItemFn: https://docs.rs/syn/1.0.19/syn/struct.ItemFn.html

[10]

syn::Signature: https://docs.rs/syn/1.0.19/syn/struct.Signature.html

[11]

Punctuated: https://docs.rs/syn/1.0.18/syn/punctuated/struct.Punctuated.html

[12]

FnArg: https://docs.rs/syn/1.0.18/syn/enum.FnArg.html

[13]

PatType: https://docs.rs/syn/1.0.18/syn/struct.PatType.html

[14]

Type: https://docs.rs/syn/1.0.18/syn/enum.Type.html

[15]

syn::ReturnType: https://docs.rs/syn/1.0.19/syn/enum.ReturnType.html

欢迎关注微信公众号: Rust碎碎念

00344822abd1eebfeeeb40c62f5042ca.png

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

错误: 至少有一个需要的隐性或转发依赖函数没找到。_【翻译】自动柯里化Rust函数... 的相关文章

  • 人工智能基础教育范围

    当前世界文明正处于从网络时代迈向智慧时代的转型过程中 人工智能对我们的生产 生活 学习方式的影响日渐深入 格物斯坦表示即便是行业人士 也是看得眼花缭乱 未来 谁主宰人工智能的核心 谁将引领时代的潮流 要培养智能时代的合格公民 人工智能教育普
  • vue+element ui +axios点击列表页列表跳转指定id详情页并进行数据渲染(纯前端)

    目录 一 思路 二 实现 一 思路 在列表页获取到所点击的list的id后 将这个id与路由绑定起来 进行跳转 渲染 不过这个方法有个bug 就是当列表使用的不是
  • 如何修改linux的MAC地址

    步骤如下 但不唯一 1 临时性的修改 依次输入以下命令 sbin ifconfig eth0 down sbin ifconfig eth0 hw ether 00 0C 29 36 97 20 sbin ifconfig eth0 up
  • 今夜喜雨

    雨夜 用了两个小时的时间终于推导了直线三角化的公式 直线三角化 是的 即通过两帧的匹配直线得到空间直线的端点 如果端点严格匹配那么就是点的三角化 你可以从 SLAM十四讲 一书或者其他的博客很容易知道怎么做 但是直线的端点往往不是严格对齐的
  • 最跌宕也最精彩, Poly Network开启加密世界新征程

    毫无疑问 未来跨链协议在整个加密生态系统中的角色将会越来越重要 但同时 一旦出现问题影响范围也相对较大 因此必须要把防范黑客恶意入侵放在首位 并且持续做好必要的安全防护措施 与此同时 安全工作也要成为常态 比如定期进行安全测试 演练 做到未
  • 史上最全的Linux常用命令汇总(超全面!超详细!)收藏这一篇就够了!

    在学习Linux时 会发现Linux的命令非常多而且还有很多参数 让人很是苦恼 就想着有没有一篇文章可以整理出来常用的命令 尽管网上有很多文章 但是总会有点问题 整理的文章非常详细但是没有目录 不能直接跳转到相关的位置 那么今天博主就带来了
  • Ubuntu的几个初始化文件

    转载自 url http kimva blogbus com logs 19989279 html url 1 etc profile 全局 公有 配置 不管是哪个用户 登录时都会读取该文件 2 ect bashrc Ubuntu没有此文件
  • 官方力推!学习Python必备的8本书

    在过去一年里 Python的热度一路飙升 国内越来越多的人选择学习Python 如今已然成为大量开发者推荐的入门编程语言和第二编程语言 而且Python还是人工智能的主要编程语言 因此 其重要性和流行度也就不言而喻了 想要学好Python语
  • 计时函数(基本知识)

    计时函数的运用 在算法题提交的时候 系统经常会给你一个TLE 这提醒我们要优化算法 但我们如何优化算法这是一个问题 我们可以运用计时函数 大概统计一下代码在最糟糕的情况下的运行时间 可以大概得出优化方向 当然这里只是大概 当超时时间过多时
  • slf4j 如何启动logback,log4j源码分析

    历史 知道java 日志框架的 应该知道log4j logback slf4j竟然是一个作者写的 我的天 为啥要写三个 这个可以去看看相关的文章 今天讨论的门面slf4j是如何驱动logback和log4j的 入口 一般日志的开启都是在这里
  • FreeRTOS 源码注释(prvCheckTasksWaitingTermination)

    FreeRTOS 中删除任务是分两步的 第一步 vTaskDelete 将其从各个工作链表中断开 放到xTasksWaitingTermination 链表中 第二步 Idle Task 调用这个函数真正的删除任务 代码很简单 不需要注释了
  • 算法、设计模式和原则

    面试总结 目录 1 设计模式 2 面向对象设计 OOD 原则 开放关闭原则 Open Closed Principle 里氏替代原则 Liskov Substitution Principle 接口隔离原则 Interface Segreg
  • PCL 逐点插入法构建Delaunay三角网(C++详细过程版)

    目录 一 逐点插入算法 二 代码实现 三 结果展示 四 测试数据 Delaunay三角剖分分为直接三角剖分和间接三角剖分 间接三角剖分首先计算为Voronoi图 然后由Voronoi图产生Delaunay三角网 这种方法的算法复杂 内存开销
  • quantopian寻找策略之mean_reversion

    股价有向均线回归的趋势 利用这个特点 可以在技术指标处于超卖阶段寻找那些上涨速度快的流通性好的股票买入 形成下面的策略 策略来源quantopian 对于市场上流通性最好的1500只股票在pipeline中先进行一波过滤 1 年收益率排名前
  • pyinstaller 打包pyqt5 之报错 no module named pyqt5.sip

    使用PyQt5做了一个GUI程序 在ubuntu 18运行源码没有问题 但是打包以后报错 虽然在我的程序中没有显式的使用到PyQt5 sip 但是PyQt5 Qtcore中会导入该模块 no module named pyqt5 sip 下
  • java 运行 加载jar_如何在运行时加载JAR文件

    用现有数据重新加载现有类可能会破坏一些东西 您可以相对容易地将新代码加载到新的类加载器中 ClassLoader loader URLClassLoader newInstance new URL yourURL getClass getC
  • JavaScript 的 Worker API 实现代码

    JavaScript 的 Worker API 允许你在后台运行脚本 从而不影响页面的性能 以下是一个简单的例子 创建一个名为 worker js 的文件 并在里面编写要在后台运行的脚本 self addEventListener mess
  • AutoSar标准下ADC的结果访问(Adc_ReadGroup与Adc_GetStreamLastPointer)的异同

    系列文章目录 等待更新中 文章目录 系列文章目录 前言 1 什么是ADC的结果访问 2 ADC访问模式 3 AUTOSAR标准配置案例分析 3 1 Configuration 3 2 图形展示结果指针初始化 3 3 使用Adc GetStr
  • 数据集下载与保存

    数据集下载与保存 使用如下代码发现无法正常下载 import torchvision from torch utils tensorboard import SummaryWriter dataset transform torchvisi

随机推荐

  • openSUSE Leap 15.4 防火墙开启与关闭,端口开启与关闭

    openSUSE Leap 15 4 防火墙开启与关闭 端口开启与关闭 1 查看防火墙状态 开启 关闭 查看防火墙当前状态 rcfirewalld status 示例结果 当前状态为开启中 开启防火墙 rcfirewalld start 关
  • 安卓APP_ 布局(2) —— RelativeLayout相对布局

    摘自 安卓APP 布局 2 RelativeLayout相对布局 作者 丶PURSUING 发布时间 2021 04 05 20 19 54 网址 https blog csdn net weixin 44742824 article de
  • 解决Nginx部署Vue项目第一次访问正常第二次访问404的问题

    location add header Access Control Allow Origin root data static project dist try files uri uri index html index index h
  • DNS资源记录详解

    资源记录 resourse record 就是域名服务器保存的记录 也是解析器请求的内容 资源记录保存在zone文件中 域 domain 以 www baidu com 为例 com是一个域 baidu com是一个域 他是com域的一个子
  • 啥是ddl?

    我们在安装软件的时候 经常出现某一个dll文件丢失 无法安装 让人头痛不已 那么到底啥是ddl 还是有度娘好呀 下面是百度百科的词条 数据库模式定义语言DDL Data Definition Language 是用于描述数据库中要存储的现实
  • NFTScan NFT API 在 NFTFi 开发中的应用

    NFTFi 是 NFT 和 Finance 的缩写 旨在 增加 NFT 流动性 提供现金流 NFTFi 是为 NFT 提供金融实用性的去中心化协议和应用程序的新兴生态系统 及使用 NFT 作为基础层在其上建设经济基础设施 在实践中 NFTF
  • 7.7 SHEIN希音笔试

    投的Java工程师岗位 题型为10道单选 5道多选 1道算法题 1道sql题 时长1h 选择题 1 spring事物的传播特性 错误的是 PROPAGATION REQUIRED 支持当前事务 如果当前没有事务 就抛出异常 应该是 支持当前
  • wireshark流量分析网络安全

    目录 前言 题目1 题目2 题目3 题目4 题目5 题目6 题目7 题目8 前言 这是关于一篇wireshark分析数据包的文章 主要是网络安全里的应用 讲述了wireshark的使用方法 主要使用的事wrieshark里的追踪流 欢迎大家
  • elementUI 下拉框select 为多选 赋值回显问题

    下拉框为多选时 绑定的值为数组状态 所以点击编辑赋值时 需要处理为数组 但我遇到的问题是 可以赋值 但不是想要的结果 我想要显示label 但显示的是value 不想得到的效果 想要的是 在赋值后面加 map Number 就可以对号入座了
  • 【翻译】 使用 SFrame 进行可靠的用户空间堆栈跟踪

    请考虑订阅 LWN订阅是 LWN net 的生命线 如果您喜欢这些内容并希望看到更多 您的订阅将有助于确保 LWN 继续蓬勃发展 请访问此页面加入我们 让 LWN 继续在网络上传播 作者 Jonathan Corbet 2023 年 5 月
  • html页面传list,后端list集合中的数据传递到前台HTML中显示(表格形式)

    关键字 web项目中前后台数据传递问题 在学习web项目的过程中 我们肯定会遇到前后台数据交换问题 这个问题我也思考了很久 今天借此总结一下 由于博主水平有限 如有不当之处 还请大家多多指正 废话不所说进入正题 一 HTML页面通过ajax
  • 《Thinking_in_java_4th》持续输出中.......

    目录标题 一 文章目录 二 源码链接 一 文章目录 Java设计者们说过 设计这门语言的灵感主要来自C Java编程思想 第 2章 一切都是对象 Java编程思想 第 4章 控制执行流程 Java编程思想 第14章 类型信息 Java编程思
  • 排名前 16 的 Java 工具类

    原链接 https mp weixin qq com s s6IfovcE LGlZJxIKfT dw 目录 org apache commons io IOUtils org apache commons io FileUtils org
  • 磁耦隔离与传统隔离的区别

    磁耦隔离与传统隔离的区别 传统隔离技术 传统的隔离方式有哪些 这里有三种通常的隔离技术 光电隔离 变压器隔离 磁耦是芯片级变压器隔离技术 电容隔离 在体积 成本 性能等各方面都有优缺点 传统的隔离方式是光电隔离 什么是光耦 什么是光隔离 光
  • Qt GraphicsView框架中实现多个item之间的层次调整功能

    目的 要实现GraphicsView中多个item之间的层次调整功能 即 选中的item可以实现 移动至顶层 移动至底层 上移一层 下移一层 等功能 之前盲目地认为Qt API会提供 获取与之相邻的sibling item 类似这样的接口
  • 2023全新SF授权系统源码 V3.7全开源无加密版本,亲测可用

    2023全新SF授权系统源码 V3 7全开源无加密版本 网站搭建很简单 大致看来一下应该域名解析后上传源码解压 访问域名 install就能直接安装 程序功能简介 1 盗版入库 26种 2 快捷登录 3 采用layuiadmin框架 4 易
  • ASP.NET core MVC动作过滤器执行顺序

    using Microsoft AspNetCore Mvc Filters using System using System Threading Tasks namespace dotnet core Filter public cla
  • 两片74161实现60进制_74LS161设计60进制计数器-数电课程设计

    计数器是一个用以实现计数功能的时序部件 它不仅可用来及脉冲数 还常用作数子系统的定时 分频和执行数字运算以及其它特定的逻辑功能 计数器种类很多 按构成计数器中的各触发器是否使用一个时钟脉冲源来分 有同步计数器和异步计数器 根据计数制的不同
  • js怎么改变样式中的属性值

    可以使用JavaScript来改变HTML元素的样式属性值 具体方法如下 通过id属性获取要修改的元素对象 var obj document getElementById element id 修改元素的样式属性值 obj style pr
  • 错误: 至少有一个需要的隐性或转发依赖函数没找到。_【翻译】自动柯里化Rust函数...

    原文标题 Auto currying Rust Functions 原文链接 https peppe rs posts auto currying rust functions 公众号 Rust碎碎念 本文包含Rust中过程宏 proced