🦀

ChatGPTにRustの勉強を手伝ってもらう - Enum編 -

2023/06/10に公開

これは何

普段Rustの勉強はThe Rust Programming Languageを読み進める形ですすめています。

しかしただ読むだけだと片っ端から以前の内容を忘れていき身になった感じがしないため、ChatGPTに練習問題を作成してもらうことにしました。
ここではChatGPTがどんな問題を作ってくれて、それに対してどう回答したかみたいな内容を記録していこうと思っています。

Enum編

GW挟んだせいでEnumについて全て忘れました。もう一回読み直します
https://doc.rust-lang.org/book/ch06-00-enums.html

読み直した上で練習問題

ChatGPTに練習問題作ってくれないか相談

I'm trying to get myself familiar with enum in Rust.
Can you give me some exercises??

快諾の上以下お題を作成してくれました

Exercise 1:
Create an enum called Direction that represents the four cardinal directions: North, South, East, and West. Implement a function called opposite_direction that takes a Direction as input and returns the opposite direction. For example, if the input is Direction::North, the function should return Direction::South.

Exercise 2:
Create an enum called Shape that represents different shapes: Circle, Square, and Triangle. Implement a function called calculate_area that takes a Shape as input and returns the area of the shape. Assume that the shapes are centered at the origin and have a unit radius/side length. You can use simple formulas to calculate the areas (e.g., πr^2 for a circle).

Exercise 3:
Create an enum called Result<T, E> that represents the result of an operation. It should have two variants: Ok(T) to indicate a successful result with a value of type T, and Err(E) to indicate an error with a value of type E. Implement a function called divide that takes two integers as input and returns a Result indicating the result of the division. If the second integer is zero, return an Err variant with an appropriate error message; otherwise, return an Ok variant with the result of the division.

Exercise 1の回答

Directionというenumを作成し、方角をvariantとするという問題。
加えてDirectionに付随するoppositeというfunctionを定義、呼ばれた際にはparameterとして受け取ったDirectionとは反対の方角を返すように実装する、とのこと。

とりあえず以下のように実装してみました。

fn main() {
    #[derive(Debug)]
    enum Direction {
        North,
        East,
        South,
        West
    }
    
    impl Direction {
        fn opposite(&self) -> Direction {
            match self {
                Direction::North => Direction::South,
                Direction::East => Direction::West,
                Direction::South => Direction::North,
                Direction::West => Direction::East,
            }
        }
    }
    
    let north = Direction::North;
    println!("{:?}", north.opposite());
}

ChatGPTの反応。褒めてくれる、うれしい。

Your solution for Exercise 1 looks great! You've defined the Direction enum with its four variants: North, East, South, and West. The opposite method is implemented for the Direction enum, allowing you to find the opposite direction based on the current direction.

しかしここで、「あれこれassociated functionとして定義したけど、別にその必要なくてむしろ普通のfunctionとして作成した方がよかったのでは、、?」という疑問が。
ChatGPTに聞いてみると、

I apologize for any confusion caused by the way the question was presented. You are correct that the wording of the exercise could have been clearer. In the exercise, you were asked to implement a function called opposite_direction that takes a Direction as input and returns the opposite direction.

While implementing opposite_direction as an associated function of the Direction enum is one way to approach the exercise, it is not the only valid approach. Defining opposite_direction as a standalone function would be perfectly acceptable as well.

Here's an example of implementing opposite_direction as a standalone function:

#[derive(Debug)]
enum Direction {
    North,
    South,
    East,
    West,
}

fn opposite_direction(dir: Direction) -> Direction {
    match dir {
        Direction::North => Direction::South,
        Direction::South => Direction::North,
        Direction::East => Direction::West,
        Direction::West => Direction::East,
    }
}

fn main() {
    let current_direction = Direction::North;
    let opposite = opposite_direction(current_direction);
    println!("Opposite direction: {:?}", opposite);
    println!("Current direction: {:?}", current_direction);
}

ChatGPT的にはどっちでもよかったらしく、methodではない実装例をあげてくれた。

Exercise 2の回答

Shapeというenumを作成し、variantにはCircleSquareTriangleを。
加えてShapeに付随するcalculate_areaというfunctionを定義し、parameterとして受け取ったShapeの面積を求めるというもの。

今回は面積を求めるということなので、Selfを最初のパラメータとして受け取ることができるassociated functionで定義しちゃって良さそうだなと考え以下のように実装

fn main() {
    #[derive(Debug)]
    enum Shape {
        Circle {
            radius: f64
        },
        Square {
            width: f64,
            height: f64
        },
        Triangle {
            base: f64,
            height: f64
        }
    }
    
    impl Shape {
        fn calculate_area(&self) -> f64 {
            match self {
                Shape::Circle {radius} => radius * 2.0 * 3.14,
                Shape::Square {width, height} => width * height,
                Shape::Triangle {base, height} => (base * height) / 2.0,
            }
        }
    }
    
    let circle = Shape::Circle {radius: 3.0};
    println!("Circle: {:?}", circle.calculate_area());
    
    let square = Shape::Square {width: 2.0, height: 2.0};
    println!("Square: {:?}", square.calculate_area());
    
    let triangle = Shape::Triangle {base: 5.0, height: 20.0};
    println!("Triangle: {:?}", triangle.calculate_area());
}

またしても褒めてくれる。

Your solution for Exercise 2 is well done! You've defined the Shape enum with three variants: Circle, Square, and Triangle. Each variant contains the necessary fields to represent the corresponding shape.

しかしながらより良い解があるのではと、実装例を見せて欲しいとお願いすると

enum Shape {
    Circle(f64),
    Square(f64),
    Triangle(f64, f64),
}

impl Shape {
    fn calculate_area(&self) -> f64 {
        match self {
            Shape::Circle(radius) => std::f64::consts::PI * radius.powi(2),
            Shape::Square(side_length) => side_length.powi(2),
            Shape::Triangle(base, height) => 0.5 * base * height,
        }
    }
}

fn main() {
    let circle = Shape::Circle(3.0);
    println!("Circle: {:?}", circle.calculate_area());

    let square = Shape::Square(2.0);
    println!("Square: {:?}", square.calculate_area());

    let triangle = Shape::Triangle(5.0, 20.0);
    println!("Triangle: {:?}", triangle.calculate_area());
}

確かにChatGPTの回答通りtuple like・struct likeどちらで値を持たせるかはおいておいて、少なくともCircleSquareについては持たせる値は一つで良かったはず。
加えてstd::f64::consts::PIとかside_length.powi(2)といった便利な書き方を教えてくれるのありがたい。

Exercise 3の回答

Result<T, E>というenumを作成し、variantにはOkay(T)Err(E)を。
次にdevideというfunctionを作成。パラメータで2つのintegerを受け取り、以下の処理を実施する。

  • 2つ目のパラメータが0の場合Errでラップしたエラーメッセージを返す。
  • それ以外の場合は割り算の結果をOkayでラップして返す

以下実装。devideの中match使用せずにifで十分なはずと思いながらも使ってみたかったのでmatch使用して実装をしてみる。

fn main() {
    #[derive(Debug)]
    enum Result<T, E> {
        Okay(T),
        Err(E)
    }
    
    fn divide(num1: i32, num2: i32) -> Result<i32, String> {
            match num2 {
                0 => Result::Err(String::from("Can't take 0, sorry m8")),
                _ => Result::Okay(num1 / num2)
            }
        }
    
    let calc1 = divide(9, 3);
    println!("Calc 1: {:?}", calc1);
    
    let calc2 = divide(5, 0);
    println!("Calc 2: {:?}", calc2);
}

やっぱり褒めてくれる。

Your solution for Exercise 3 is well done! You've defined the Result<T, E> enum, which is similar to the built-in Result enum in Rust. It has two variants: Okay(T) to represent a successful result with a value of type T, and Err(E) to represent an error with a value of type E.

ChatGPTにも実装をするようお願いすると、

enum Result<T, E> {
    Ok(T),
    Err(E),
}

fn divide(num1: i32, num2: i32) -> Result<i32, String> {
    if num2 == 0 {
        Result::Err(String::from("Can't divide by zero"))
    } else {
        Result::Ok(num1 / num2)
    }
}

fn main() {
    let calc1 = divide(9, 3);
    match calc1 {
        Result::Ok(result) => println!("Calc 1: {:?}", result),
        Result::Err(err) => println!("Calc 1 error: {:?}", err),
    }
    
    let calc2 = divide(5, 0);
    match calc2 {
        Result::Ok(result) => println!("Calc 2: {:?}", result),
        Result::Err(err) => println!("Calc 2 error: {:?}", err),
    }
}

devideの中ではifを使ってその後Resultから帰ってきた値を抜き出すところでmatchを使ってる。
最初の自分の回答ではResultの中から値を抜き出すことはせずそのままprintしているため、ChatGPTの方がより丁寧な印象。
matchの使い所もこっちの方が正しい気がする

以上ChatGPTにRustの勉強を手伝ってもらう - Enum編 - でした!
聞き方や扱うトピックによって難易度に若干のばらつきはあるものの、読んだチャプターの振り返・書き方しっかり覚えたい時などなど、これからも重宝しそうな使い方だなぁという印象でした。

Discussion