본문 바로가기
프로그램 활용/인공지능(AI)

기초_ rust for AI

by 3604 2024. 6. 15.
728x90

출처: Rust for Artificial Intelligence: A Basic Guide | Reintech media

 

TensorFlow, a powerful open source machine learning framework developed by the Google Brain team, has become a cornerstone in artificial intelligence. While traditionally associated with languages like Python, the advent of Rust, a systems programming language valued for its performance and safety, has opened new avenues for TensorFlow enthusiasts.

In this guide, we will explore the fusion of TensorFlow and Rust, delving into how we can integrate these two technologies to harness the strengths of both.

Setting up our TensorFlow boilerplate

All the code discussed in this article is available, and ready to run, in this GitHub repository. The boilerplate for TensorFlow is simple — add the following dependency in the Cargo.toml file:

[dependencies]
tensorflow = "0.21.0"

In case you want to use the GPU, just use the tensorflow_gpu feature in your Cargo.toml:

[dependencies]
tensorflow = { version = "0.21.0", features = ["tensorflow_gpu"] }

This is the only dependency we will need for the examples in the following sections. Just to verify that everything works, check the following program (you will find it in the directory tf-example1 in the repository):

extern crate tensorflow;

use tensorflow::Tensor;

fn main() {
    let mut x = Tensor::new(&[2]);
    
    x[0_usize] = 3.0f32;
    x[1_usize] = 2.0f32;

    println!("{:?}", x);
}

The program is simple but useful to check that everything is in place. Let’s take a deeper look at it. First we declare an external crate named tensorflow, indicating that the program will use the TensorFlow crate.

We then import the Tensor type from the TensorFlow crate. A tensor in TensorFlow represents a multidimensional array and is a fundamental data structure for computations. For a general introduction to TensorFlow concepts, you can refer to the official documentation.

The main function creates a new mutable tensor x with a one-dimensional vector with two elements. In TensorFlow, the shape of a tensor specifies the number of elements in each dimension: if you specify, for example, [2,3], the tensor will (unsurprisingly 

) have the shape of a 2×3 matrix.

Lastly, we just assign values to the elements of the tensor x. In this case, it sets the first element to 3.0 and the second element to 2.0. Finally, the program prints the tensor x using the println! macro. {:?} is a formatting specifier to print the tensor in a debug format.

Project overview and understanding the XOR function

Training a neural network to learn the XOR (exclusive OR) function is a classic example that highlights the capability of neural networks to learn complex relationships in data. XOR is a binary operation that outputs true (1) only when the number of true inputs is odd.

No matter how simple it is conceptually, the XOR example will help show us all the necessary steps to design, train, and use a model. Learning the XOR table of truth justifies the use of a hidden layer in the neural network; indeed, simpler networks, like a single neuron, cannot learn the XOR table of truth. So the XOR example is both simple to program and requires all the power of more complex neural networks.

The following code is in the directory tf-example2. The code has two functionalities, as you may expect: the training of the network, in the train function; and the inference, in the eval function. As an additional functionality, the training saves the trained model to let it be used later on.

Building our network with TensorFlow and Rust

Let’s get right into the code:

fn train<P: AsRef<Path>>(save_dir: P) -> Result<(), Box<dyn Error>> {
    // ================
    // Build the model.
    // ================
    let mut scope = Scope::new_root_scope();
    let scope = &mut scope;
    // Size of the hidden layer.
    // This is far more than is necessary, but makes it train more reliably.
    let hidden_size: u64 = 8;
    let input = ops::Placeholder::new()
        .dtype(DataType::Float)
        .shape([1u64, 2])
        .build(&mut scope.with_op_name("input"))?;
    let label = ops::Placeholder::new()
        .dtype(DataType::Float)
        .shape([1u64])
        .build(&mut scope.with_op_name("label"))?;
    // Hidden layer.
    let (vars1, layer1) = layer(
        input.clone(),
        2,
        hidden_size,
        &|x, scope| Ok(ops::tanh(x, scope)?.into()),
        scope,
    )?;
    // Output layer.
    let (vars2, layer2) = layer(layer1.clone(), hidden_size, 1, &|x, _| Ok(x), scope)?;
    let error = ops::sub(layer2.clone(), label.clone(), scope)?;
    let error_squared = ops::mul(error.clone(), error, scope)?;
    let mut optimizer = AdadeltaOptimizer::new();
    optimizer.set_learning_rate(ops::constant(1.0f32, scope)?);
    let mut variables = Vec::new();
    variables.extend(vars1);
    variables.extend(vars2);
    let (minimizer_vars, minimize) = optimizer.minimize(
        scope,
        error_squared.clone().into(),
        MinimizeOptions::default().with_variables(&variables),
    )?;

We begin by constructing a neural network model using TensorFlow. We must build the computation graph using Placeholder for input (the input variable) and target labels (the label variable).

In TensorFlow, a Placeholder serves as a variable to which data will be assigned when the Session begins (we’ll explore this in depth in the next section). This functionality allows the creation of processes or operations without an immediate need for data.

Then, two layers are created: a hidden layer (layer1) and an output layer (layer2). We do this with a handy function called layer that takes five input parameters:

  1. The input layer (the layer to which we want to attach the new layer)
  2. The size of the input layer
  3. The output size (the number of new nodes we’ll create in the new layer)
  4. The activation function of the newly created tensors
  5. The scope within the new layer it will be created
// Returns variables created and the layer output.
fn layer<O1: Into<Output>>(
    input: O1,
    input_size: u64,
    output_size: u64,
    activation: &dyn Fn(Output, &mut Scope) -> Result<Output, Status>,
    scope: &mut Scope,
) -> Result<(Vec<Variable>, Output), Status> {
    let mut scope = scope.new_sub_scope("layer");
    let scope = &mut scope;
    let w_shape = ops::constant(&[input_size as i64, output_size as i64][..], scope)?;
    let w = Variable::builder()
        .initial_value(
            ops::RandomStandardNormal::new()
                .dtype(DataType::Float)
                .build(w_shape, scope)?,
        )
        .data_type(DataType::Float)
        .shape([input_size, output_size])
        .build(&mut scope.with_op_name("w"))?;
    let b = Variable::builder()
        .const_initial_value(Tensor::<f32>::new(&[output_size]))
        .build(&mut scope.with_op_name("b"))?;
    Ok((
        vec![w.clone(), b.clone()],
        activation(
            ops::add(
                ops::mat_mul(input, w.output().clone(), scope)?,
                b.output().clone(),
                scope,
            )?
            .into(),
            scope,
        )?,
    ))
}

The result of calling the layer function is depicted in the following figure:

The first layer is created by calling the layer function using the Placeholder named input (containing two nodes) as the first parameter. This creates a subgraph that has two input nodes and an output layer containing hidden_size nodes (the left part of the figure below). The result is assigned to the object named layer1 to use in the next step of the construction.

The rightmost subgraph in the figure above is created by calling the layer function with the layer1 object as the first parameter. This will “stitch” the new layer on the rightmost part of layer1. The new layer will contain one single node: the output node for the whole network.

The activation function for the newly created tensors is tanh. You can see this by looking at the last parameter before scope where ops::tanh is passed as functional to the layer function; it is returned in the Ok value from the layer function. Together with the activation function, it also returns a vector containing all the tensors of the new layer.

As you can see, the layer function is quite handy and can be used iteratively to compose more complex graphs.

Training our network

After creating the network, we prepare SaveModelBuilder to be able to save the model once it is trained. For future reference, the model will be saved in the temporary directory on your disk — typically, it’s /tmp — and it will be named tf-rust-example-xor-saved-model.

The first step of the session is the initialization of all the variables. Before you can use operations in your model, you need to explicitly run variable initializers. The way we want to initialize our variable is set up by the layer function.




The explicit initialization is carried out by adding the invocation to the proper initializer op to the session. At this point, it is useful to explain the two concepts we are playing with in this code — graph and session:

  • A graph outlines what computations to do. It doesn’t calculate anything or store values; it just describes the operations you wrote in your code. It is a static concept
  • A session enables the execution of graphs or parts of them. It manages resources, possibly on multiple machines, and stores the real values of interim results and variables. It is a dynamic object that evolves

After setting up the initialization, we can start adding the proper training ops and execute the session. This is performed by another handy function, train, which is called 10,000 times. Let’s look at the train function:

    let mut train = |i| -> Result<f32, Box<dyn Error>> {
        input_tensor[0] = (i & 1) as f32;
        input_tensor[1] = ((i >> 1) & 1) as f32;
        label_tensor[0] = ((i & 1) ^ ((i >> 1) & 1)) as f32;
        let mut run_args = SessionRunArgs::new();
        run_args.add_target(&minimize);
        let error_squared_fetch = run_args.request_fetch(&error_squared, 0);
        run_args.add_feed(&input, 0, &input_tensor);
        run_args.add_feed(&label, 0, &label_tensor);
        session.run(&mut run_args)?;
        Ok(run_args.fetch::<f32>(error_squared_fetch)?[0])
    };

The train function takes an integer as input — the variable i — that is used to generate the input and the label for the graph. The input is prepared (intuitively, we take the first two bits of the i variable as input), and we calculate the label, which is the expected output of our network — in this case, of course, the XOR of the two input bits.

Then, the ops for feeding the graph and fetching the result are added at the session. The actual training is performed by executing the optimizer op that we added to the graph in the layer function.

Once the model is trained, we can save the model to reuse it later on without the need to retrain it. This is overkill for the XOR, but in general, it is always a good idea to save all the hard work for later.

Another point to discuss: the training here is made on a fixed number of training steps, but in the most general case, of course, you should adopt a different policy where you keep training as long as the error becomes smaller than a given threshold. Take a look at the following lines to get an idea of how to implement this:

    for i in 0..4 {
        let error = train(i)?;
        println!("Error: {}", error);
        if error > 0.1 {
            return Err(Box::new(Status::new_set(
                Code::Internal,
                &format!("Error too high: {}", error),
            )?));
        }
    }
    Ok(())

Evaluating our model

The evaluation of the training is carried out in the eval function, as you can see here:

fn eval<P: AsRef<Path>>(save_dir: P) -> Result<(), Box<dyn Error>> {
    let mut graph = Graph::new();
    let bundle = SavedModelBundle::load(
        &SessionOptions::new(),
        &["serve", "train"],
        &mut graph,
        save_dir,
    )?;
    let session = &bundle.session;
    let signature = bundle.meta_graph_def().get_signature(REGRESS_METHOD_NAME)?;
    let input_info = signature.get_input(REGRESS_INPUTS)?;
    let output_info = signature.get_output(REGRESS_OUTPUTS)?;
    let input_op = graph.operation_by_name_required(&input_info.name().name)?;
    let output_op = graph.operation_by_name_required(&output_info.name().name)?;

    let mut input_tensor = Tensor::<f32>::new(&[1, 2]);
    for i in 0..4 {
        input_tensor[0] = (i & 1) as f32;
        input_tensor[1] = ((i >> 1) & 1) as f32;
        let expected = ((i & 1) ^ ((i >> 1) & 1)) as f32;
        let mut run_args = SessionRunArgs::new();
        run_args.add_feed(&input_op, input_info.name().index, &input_tensor);
        let output_fetch = run_args.request_fetch(&output_op, output_info.name().index);
        session.run(&mut run_args)?;
        let output = run_args.fetch::<f32>(output_fetch)?[0];
        let error = (output - expected) * (output - expected);
        println!("Error: {}", error);
        if error > 0.1 {
            return Err(Box::new(Status::new_set(
                Code::Internal,
                &format!("Error too high: {}", error),
            )?));
        }
    }

    Ok(())
}

As you may expect, it’s nothing fancy. It simply loads the trained model we saved before and uses it to feed four integer numbers (once again, we will use the two less significative bits as inputs for the network) and fetch the result from the graph together with the calculated error.

First, we load the saved model; then, we recover, from the saved model bundle, the input and output operations from the graph. In the for loop, we assemble the input, feed it to the graph, and add the fetch operation.

Once the session is run, we get the label from the output tensor and the calculated error between the value calculated from the model and the expected value we’ve calculated. You can see the output of the execution in the following figure:

Conclusion

In this article, we summarize two simple but relevant examples of using TensorFlow with Rust. The intent is to have an example simple enough not to get into the trouble of understanding complex models, yet complex enough to understand the concepts of building a network, training it, saving it, and loading the trained model.

LogRocket: Full visibility into web frontends for Rust apps

Debugging Rust applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking the performance of your Rust apps, automatically surfacing errors, and tracking slow network requests and load time, try LogRocket.

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your Rust application. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.

Modernize how you debug your Rust apps — start monitoring for free.

 

 

ㅁ 참고 자료 - 실행 안됨.

Rust for Artificial Intelligence: A Basic Guide

Welcome to this comprehensive guide on using Rust for Artificial Intelligence (AI). Rust is a modern, high-performance language that guarantees memory safety without a garbage collector. Its unique approach to memory management makes it a promising language for AI, Machine Learning (ML), and Data Science.

Getting Started with Rust

Before diving into AI, let's ensure Rust is installed on your system. Open your terminal and type:

 curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

This command will download a script and start the installation of the rustup toolchain installer.

Basic Syntax

For AI programming, you'll need a firm understanding of Rust's basic syntax. Here's a simple "Hello, World" program:

fn main() {
    println!("Hello, World!");
}

Rust Libraries for AI

There are several open-source libraries available for AI development in Rust, such as:

  • Autograd: Autograd-rs is a library for automatic differentiation.
  • Leaf: An open, modular machine learning framework.
  • Tensorflow: Rust bindings for Tensorflow.

Implementing a Simple AI Model in Rust

We'll use the Tensorflow Rust library to implement a simple AI model. First, add Tensorflow as a dependency in your Cargo.toml:

[dependencies]
tensorflow = "0.21.0"

Next, we'll create a simple program that creates a Tensor and prints its value:

extern crate tensorflow;

use tensorflow::{Session, Tensor};

fn main() {
    let mut x = Tensor::new(&[2]);
    x[0_i32] = 3.0f32;
    x[1_i32] = 2.0f32;

    println!("{:?}", x);
}

This is a simple example, but it illustrates the foundational concepts of working with Tensors in Rust, which is a critical element in most AI applications.

Conclusion

Rust's emphasis on performance and memory safety makes it a promising option for AI and ML applications. As the Rust ecosystem continues to mature, we can expect to see more tools and libraries emerge to support AI and ML development.

If you're planning to build an AI application and are looking for skilled developers, hire Rust developers from Reintech to ensure your project's success.

 

참고 1.

출처: 설치하기 - The Rust Programming Language (rinthel.github.io)

설치하기

첫 번째 단계는 러스트를 설치하는 것입니다. 우리는 rustup이라고 하는 러스트 버전 및 관련 도구들을 관리하기 위한 커멘드 라인 도구를 통하여 러스트를 다운로드할 것입니다. 다운로드를 위해서는 인터넷 연결이 필요할 것입니다.

다음 단계들이 러스트 컴파일러의 최신 안정 버전을 설치합니다. 이 책에 나오는 모든 예제들과 출력들은 안정화된 러스트 1.21.0을 사용했습니다. 러스트의 안정성에 대한 보장은 책에 나오는 모든 예제들이 새로운 러스트 버전에서도 계속해서 잘 컴파일 되도록 해줍니다. 버전마다 출력이 약간씩 다를 수도 있는데, 이는 러스트가 종종 에러 메시지와 경고들을 개선하기 때문입니다. 바꿔 말하면, 이 단계들을 이용하여 여러분이 설치한 러스트가 어떤 새로운 안정화 버전이라도 이 책의 내용에 기대하는 수준으로 동작해야 합니다.

커맨드 라인 표기법

이 장 및 책 곳곳에서, 우리는 터미널에서 사용되는 몇몇 커맨드를 보여줄 것입니다. 여러분이 터미널에 입력해야 하는 라인들은 모두 $로 시작합니다. 여러분은 $ 문자를 입력할 필요가 없습니다; 이는 각 커맨드의 시작을 나타냅니다. 여러분이 일반 사용자로서 실행할 커맨드를 위해 $를 그리고 여러분이 관리자로서 실행할 커맨드를 위해 #를 쓰는 관례는 많은 튜토리얼들이 사용합니다. $로 시작하지 않는 라인들은 보통 이전 커맨드의 출력을 나타냅니다. 추가적으로, 파워쉘 한정 예제는 $ 대신 >를 이용할 것입니다.

Linux와 macOS에서 Rustup 설치하기

만일 여러분들이 Linux 혹은 macOS를 사용중이라면, 터미널을 열고 다음 커멘드를 입력하세요:

 
$ curl https://sh.rustup.rs -sSf | sh

이 커맨드는 스크립트를 다운로드하고 rustup 도구의 설치를 시작하는데, 이 도구는 가장 최신의 러스트 안정화 버전을 설치해줍니다. 여러분의 패스워드를 입력하라는 프롬프트가 나올 수도 있습니다. 설치가 성공적이면, 다음과 같은 라인이 나타날 것입니다:

 
Rust is installed now. Great!

물론 여러분이 어떤 소프트웨어를 설치하기 위해 curl URL | sh를 사용하는 것을 신용하지 않는다면, 여러분이 원하는 어떤 방식으로든 이 스크립트를 다운로드하고, 검사하고, 실행할 수 있습니다.

설치 스크립트는 여러분의 다음 로그인 이후에 러스트를 자동적으로 여러분의 시스템 패스에 추가합니다. 만일 여러분이 터미널을 재시작하지 않고 러스트를 바로 사용하기를 원한다면, 다음과 같은 커멘트를 쉘에서 실행하여 수동적으로 러스트를 시스템 패스에 추가하세요:

 
$ source $HOME/.cargo/env

혹은 그 대신에, 여러분의 ~/.bash_profile에 다음과 같은 라인을 추가할 수 있습니다:

 
$ export PATH="$HOME/.cargo/bin:$PATH"

추가적으로, 여러분은 어떤 종류의 링커가 필요할 것입니다. 이미 설치되어 있을 것 같지만, 여러분이 러스트 프로그램을 컴파일하다가 링커를 실행할 수 없음을 나타내는 에러를 보게 되면, 링커를 설치해야 합니다. 여러분은 C 컴파일러를 설치할 수 있는데, 이것이 보통 올바른 링커와 함께 설치되기 때문입니다. C 컴파일러를 인스톨하는 방법을 위해서는 여러분의 플랫폼 문서를 확인하세요. 몇몇의 일반적인 러스트 패키지는 C 코드에 의존적이고 C 컴파일러 또한 사용할 것이므로, 지금 상황에 상관없이 하나 설치하는것이 좋을 수도 있습니다.

Windows에서 Rustup 설치하기

Windows에서는 https://www.rust-lang.org/en-US/install.html 페이지로 가서 러스트 설치를 위한 지시를 따르세요. 설치의 몇몇 지점에서, 여러분이 Visual Studio 2013이나 이후 버전용 C++ 빌드 도구 또한 설치할 필요가 있음을 설명하는 메세지를 받을 것입니다. 이 빌드 도구를 얻는 가장 쉬운 방법은 Visual Studio 2017용 빌드 도구를 설치하는 것입니다. 이 도구들은 다른 도구 및 프레임워크 섹션 내에 있습니다.

이 책의 나머지 부분에서는 cmd.exe 및 파워쉘 모두에서 동작하는 커멘드를 사용합니다. 만일 특별히 다른 부분이 있다면, 어떤 것을 이용하는지 설명할 것입니다.

Rustup 없이 커스텀 설치하기

만일 여러분이 어떤 이유로 rustup를 쓰지 않기를 선호한다면, the Rust installation page 페이지에서 다른 옵션을 확인하세요.

업데이트 및 설치 제거하기

rustup을 통해 러스트를 설치한 뒤라면, 최신 버전을 업데이트하는 것은 쉽습니다. 여러분의 쉘에서 다음과 같은 업데이트 스크립트를 실행하세요:

 
$ rustup update

러스트와 rustup을 제거하려면 다음과 같은 설치 제거용 스크립트를 쉘에서 실행하세요:

 
$ rustup self uninstall

문제 해결하기

러스트가 올바르게 설치되었는지를 확인하기 위해서는, 쉘을 열고 다음 라인을 입력하세요:

 
$ rustc --version

버전 번호, 커밋 해쉬, 그리고 배포된 최신 안정 버전에 대한 커밋 일자가 다음과 같은 형식으로 보여야 합니다:

 
rustc x.y.z (abcabcabc yyyy-mm-dd)

이 정보가 보인다면, 여러분은 러스트를 성공적으로 설치한 것입니다! 만일 이 정보가 보이지 않고 Windows를 이용중이라면, %PATH% 시스템 변수 내에 러스트가 있는지 확인해주세요. 만일 이 설정이 모두 정확하고 러스트가 여전히 동작하지 않는다면, 여러분이 도움을 구할 수 있는 몇 군데의 장소가 있습니다. 가장 쉬운 방법은 irc.mozilla.org 안에 있는 #rust IRC 채널인데, 이는 Mibbit을 통해 접속할 수 있습니다. 이 주소에서 여러분을 도와줄 수 있는 다른 러스티시안(Rustacean, 우리가 스스로를 부르는 우스운 별명입니다)들과 채팅을 할 수 있습니다. 다른 훌륭한 리소스들에는 유저 포럼 Stack Overflow가 있습니다.

로컬 문서

인스톨러에는 또한 문서 복사본이 로컬에 포함되어 있으므로, 여러분은 이를 오프라인으로 읽을 수 있습니다. 여러분의 브라우저에서 로컬 문서를 열려면 rustup doc을 실행하세요.

표준 라이브러리가 제공하는 타입이나 함수가 무엇을 하는지 혹은 어떻게 사용하는지 확신이 들지 않는다면 언제라도 API (application programming interface) 문서를 이용하여 알아보세요!

 

참고 2.

출처: Why I started learning Rust and impressions after a week (vikky.dev)

Why I started learning Rust and impressions after a week

 

 
 
 
 

I decided to learn one new programming language every year since 2019. I don't plan to become an expert in a particular language. But it introduces a lot of new patterns and broadens our perspective. In 2019, my choice was Flutter. I was advocating about Flutter to our team and did a few side projects (which are never going to see the light of the day). Flutter was a great choice for both joys of learning and the potential of building something for my own.

For 2020, I have been thinking about learning either Golang or Rust that revered by devs working on it. After some days of thought, I decided to go with Rust because of a few things,

  1. In Rust, variables are immutable by default. Having worked with JS and PHP in complex applications, I can say that this is a great feature. My understanding of the immutable state and unidirectional data-flow is low. I faced a lot of issues due to the misuse of immutable state. Rust makes it hard to make those mistakes.
  2. Rust doesn't have garbage collection. I never worked with low-level languages that need developers to manage memory. But I know how hard it can be, I spent many hours on V8 blog reading how the garbage collector for JS works in chrome. It is really interesting, how a language works, without dealing with memory management and without being GC as well.
  3. Rust has first-class support for WebAssembly. I first saw the real strength of webAssembly in Figma. Their site is one of the most complicated web application and it worked like a charm. This is what web apps have been struggling for decades. I was thinking to use WASM into real projects. But I'm not ready for implementing and supporting it in production yet. In real projects, deadlines and stability matter the most. Learning Rust will help me work with WASM and then start using them in production apps.

Once I decided to learn Rust I didn't start it until recently due to my procrastinating nature. In April, I was scrolling through twitter and decided to start with #100daysofcode. I was not a believer in the social part of learning but it helps to connect with relevant people. Also, It holds you accountable when you commit to it publicly. So give it a try, even if you're an experienced programmer.

Setup

Rust is easy to set up by using the rustup. run the below command, and let it take care of installing and configuring your environment.

COPY
COPY
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

After installing, I tried to build a hello world application and got served with this.

COPY
COPY
error: linker cc not found.

This means that rust is missing some dependency that it needs for compiling code. I was not sure about why the rustup script missed this dependency. So after some googling, I solved it by installing the build-essentials in Ubuntu.

COPY
COPY
sudo apt install build-essentials

I am using Webstorm with Rust plugin and it works well enough for me.

Learning Materials & tools,

Rust has one of the excellent official documentation and tools in my experience. It took decades for PHP to build composer and Javascript to create NPM. Rust has a command-line tool called rustc which will let you compile and run Rust programs. The best part is you don't have to use it when you have cargo. Cargo is the official package manager for Rust. The packages built by the rust community are crates. You use cargo to orchestrate the crates you want in your codebase.

If you want to check for any crates, head to creates.io, and search with the keyword. Once found, add it to your cargo.toml file. Every time you run cargo build it will install the dependencies automatically (There is no separate install command AFAIK).

Dependency Management

To manage the dependencies Rust uses two files. cargo.toml, and cargo.lock which are like npm's package.json and package-lock.json. The cargo.toml will keep the list of dependencies required for your project. It is maintained by the developer.

The cargo.lock is a detailed record of your dependencies and their dependencies. It records the exact versions installed in your machine. This is useful to reproduce the exact version of the dependency tree on other machines. You should not edit the cargo.lock file because it will be overwritten.

You might have noticed the extension of the cargo.toml file. TOML stands for "Tom's obvious, minimal language" and used for config files in rust. You can learn more about toml here.

Like the NPM ecosystem, if you're building an end-user application, you should include the cargo.lock to your VCS. If you're building a library that others will be reusing, your cargo.lock must be added to .gitignore.

Building & Releasing

We're too early in our rust journey to worry about building and releasing. Unlike JS/PHP, Rust is a compiled language. That means to run even the basic hello world example, we need to learn how to compile the code and run it. You can do that by using cargo.

cargo build command can be used to build the rust files into a binary executable. If there are any issues in your code, the compiler will throw errors. Rust compiler's error messages are really helpful. They offer a possible fix for the error if you read the full message (you should).

So assuming your code compiled, a folder named target will be created. The Target folder contains all the build artifacts and intermediate compile stage files. Keeping the intermediate files will make the next compilations faster than the first compilation.

There are two types of builds in rust, one is debugging another one is for release. The debug build is not optimized and contains data required for debugging. The release build is optimized and it won't keep anything for debugging. You can create a release build by running cargo build —release command.

Rust includes the dependencies within your final build. This means that your binary should work without any issues in most systems. But this comes with a trade-off. Including all dependencies will increase the binary size. Rust chooses maximum portability over smaller binary executables.

Language syntax & Features

Modules

Rust's idea of code reuse/sharing within a project is a module. A module is a set of functions grouped under a common namespace. There are a lot of built-in modules in rust such as std which is the standard module.

We can create our own modules within the current file or as a separate file. Conventional wisdom is group modules on their own files. For me, the syntax for importing an external module is a little confusing. I have only seen a small part of it and yet to dive deep into defining using modules. My take away here is that Rust supports what is possible in PHP for modules and importing, use statement, and as syntax.

Access Modifiers

Rust supports two types of visibility public and private. The public is defined by pub keyword but there is no separate keyword (AFAIK) for private. Wondering why?

Rust chooses private by default. This is different than all the other languages I know. In typescript and PHP public is the default. From experience, I know how hard it is when debugging or refactoring to know the actual usage of a method.

In Rust, if it is not pub it can't be used anywhere, period. This is a great design choice that aligns with the rest of the rust's design principles. It makes it impossible to make mistakes in this regard.

Variables

Variables are interesting things in rust. They are defined using let keyword. Variables are immutable by default. To make them mutable you must use the mut keyword before the variable name.

COPY
COPY
// default immutable variable
let age = 26;

// mutable variable
let mut age = 26;

The immutability as default will make you take a moment and think about the usage for every variable. In effect, you will start thinking about the collective state and data flow.

Rust is strictly typed, but we don't have to define data types for every variable. Rust will infer data types from the value or the usage of the variable. Rust's type inference is different than typescript. When the rust compiler can't guess a variable's type or if there is an ambiguity of types, the code will not compile. In that case, we must define a type for that variable and get rid of the ambiguity.

Data types

Rust's primitive data types are integers, Float, Character, Boolean. There are compound types such as Tuples, and Array. I had trouble grasping the limits of each integer type such as i8 or u8. I wasn't sure how to decide which type of integer I should use in a given place. This is important because you should not use more memory than you need.

I loved how Rust made me think about the low-level stuff such as how much memory does this variable needs. Usually, in typescript, we just type it as the number and call it done. But rust has the following data types for signed and unsigned integers.

To calculate the limit of each type, the manual gave a formula that is really useful. I must admit, that any programmer with a CS degree or anyone having experience with other low-level languages might find this trivial but it is hard for me.

A variable can store -(2^n - 1) to (2^n - 1)-1 where n is the number of bits. This formula is for signed integers. So i8 can store -128 to 127.

The isize and usize are interesting types because their size will be determined based on the architecture of the system that the code will be running. For 32-bit systems, they'll be 32 bit and 64 for the 64-bit machines.

When you try to assign a value that is bigger than the range of the integer type, It is an issue. In debug mode rust panics. Panic is rust's name for errors. In release mode, the code runs but reduces the value to the possible value that variable can store. This reducing behavior is called wrapping.

Statement vs Expressions

Rust introduces us to the difference between statement vs expressions. This difference plays a major role in how we create and return values from blocks and functions. Also in How we can assign values to a variable conditionally.

A statement is something that states that a new value is created or a new item is created in the scope. Ex.

COPY
COPY
Let number: i8 = 45;

In other languages, statements can return values, but in rust, statements can't return any values.

COPY
COPY
let a = b = 10;

the above code is valid in Javascript. Both a and b will be assigned with a value of 10. In rust, this will result in a compile error.

An expression is something that can return a value. Rust is an expression-based language. Most things you use are expressions. Such as calling a function, calling a macro, evaluating a numerical operation. Since expressions return values, you can use them on the right-hand side of an assignment. That opens a lot of possibilities and syntactic sugar.expression-based

COPY
COPY
let age = {
    let current_year = 2020;
    let year_of_birth = 2013;
    current_year - year_of_birth
};
// age = 7;

Notice that we created a new scope using {} that is an expression. You can return a value from an expression simply by not putting a ; at the last line of the expression. If you use a semicolon, the expression will turn into a statement and you'll be served with an error. This might be hard to grasp at first. It makes sense once you start writing functions with expressions.

Functions

Functions are named using snake_case and start with an fn keyword. Unlike variables, it is mandatory to define both the argument and the return types of a function. The argument is typed using the usual : symbol, function return types though uses a weird -> arrow.

Functions are made up of a series of statements and expressions. If you want to return a value you can use the return statement. But, the last expression in the function will be implicitly returned. So we may need to use the return statement only when we need an early return based on a condition.

Control Flow

Rust has if and the usual suspects such as the while and for..in. Conditionals in rust are different. In JS, if you put something in a conditional the interpreter will try to evaluate them to a truthy or a falsy value. That leads to some unexpected edge cases. But rust will not try to convert any non-boolean values to boolean and will throw a compile error instead. Rust if is an expression. so you can do this.

COPY
COPY
let number = if condition { 5 } else { 6 };

For looping, there is a construct called Loop. It just loops over the statements inside it until we stop using the break statement. All the other looping constructs can be written using just the loop and the break.

COPY
COPY
fn main() {
    loop {
        println!("All work and no play makes jack a dull boy!");
    }
}

I also learned a bit about what is ownership and how Rust does its magic without garbage collecting. That's a huge topic and I should learn about lifetime in rust before writing about ownership. For now, just assume that your variables will be removed from memory once the code ran past their scope.

Update: I make a mistake in calculating the range of i8 which had 1 bit extra, I posted it without realizing and Steven from Rust reddit community noticed and corrected it.

Final words

These are the things I thought important in my first week of learning Rust. It is exciting to see how useful rust can be in a web developer's toolkit. I am hoping to finish 3, 4 projects before the end of the year and to learn rust to an intermediate level.

Are you familiar with Rust or thinking about learning it? Comment here or DM me on @ShivEnigma. let's chat about your experience and see if we can work together on any open-source project.

728x90

'프로그램 활용 > 인공지능(AI)' 카테고리의 다른 글

유용한 ChatGPT 프롬프트 팁  (0) 2024.03.03
AI 이해 Conda란  (0) 2024.02.05
private ChatGPT 구축  (0) 2024.02.05
ChatGPT는 TensorFlow를 사용합니까?  (0) 2024.01.18
RAG 흐름  (0) 2024.01.18