vak: (Знайка)
[personal profile] vak
Есть в Rust популярная идиома: Result<(), String>. Используется для функций, которые в случае успехи ничего не возвращают, а в случае ошибки возвращают текстовую строку. К примеру, копирование одного файла в другой из реального проекта:
fn copy_file_contents(mut dest_file: &fs::File, src_filename: &str) -> Result<(), String> {
let mut src_file = fs::File::open(src_filename)
.map_err(|e| format!("Failed to open source file '{}': {}", src_filename, e))?;
io::copy(&mut src_file, &mut dest_file)
.map_err(|e| format!("Failed to copy to destination: {}", e))?;
Ok(())
}
Давайте попросим grok.com сделать аналогичный класс на Си++.

Question: There is a popular idiom: Result<(), String>. I would like to see how it could be implemented in C++.
Let’s dive into translating the Rust idiom Result<(), String> into C++. In Rust, Result is an enum used for error handling, with two variants: Ok(T) for success and Err(E) for failure. Here, Result<(), String> means a function returns either "success with no meaningful value" (Ok(()), where () is the unit type) or "failure with a string error message" (Err(String)). This is a common pattern in Rust for operations that either succeed without producing a value or fail with a descriptive error.

In C++, we don’t have a direct equivalent to Rust’s Result in the standard library, but we can approximate it using modern C++ features. The closest analogs are std::optional, std::variant, or a custom implementation.

Let’s define a reusable Result class with a Rust-like interface, using std::variant under the hood. This adds convenience methods like is_ok(), is_err(), unwrap(), and unwrap_err().
#include <variant>
#include <string>
#include <stdexcept>
#include <iostream>

template<typename T, typename E>
class Result {
public:
Result(T value) : data(value) {}
Result(E error) : data(error) {}

bool is_ok() const { return std::holds_alternative<T>(data); }
bool is_err() const { return std::holds_alternative<E>(data); }

T unwrap() const {
if (is_err()) throw std::runtime_error("Called unwrap() on an Err value");
return std::get<T>(data);
}

E unwrap_err() const {
if (is_ok()) throw std::runtime_error("Called unwrap_err() on an Ok value");
return std::get<E>(data);
}

private:
std::variant<T, E> data;
};

// Specialize for empty value and std::string
struct Void {};
using VoidResult = Result<Void, std::string>;
This version is the most Rust-like. VoidResult directly corresponds to Result<(), String>. You can call is_ok() or is_err() to check the state, and unwrap() or unwrap_err() to extract values (with error checking). It’s reusable for other T and E types too.
А вот пример использования такого класса, тоже сгенерённый Гроком.
VoidResult do_something(bool fail = false) {
if (fail) {
return std::string("Something went wrong");
}
return Void{};
}

int main() {
// Test success case
auto success = do_something(false);
if (success.is_ok()) {
std::cout << "Success! Unwrapped: Void\n";
success.unwrap(); // Works fine, returns Void
}

// Test failure case
auto failure = do_something(true);
if (failure.is_err()) {
std::cout << "Error: " << failure.unwrap_err() << "\n";
}

// Test unwrap() on failure (will throw)
try {
failure.unwrap();
} catch (const std::runtime_error& e) {
std::cout << "Caught: " << e.what() << "\n";
}

// Test unwrap_err() on success (will throw)
try {
success.unwrap_err();
} catch (const std::runtime_error& e) {
std::cout << "Caught: " << e.what() << "\n";
}

return 0;
}
Вполне разумная вещь получилась. Можно использовать в разных Си++ проектах. Полный исходник лежит здесь: result.cpp

Date: 2025-02-27 21:40 (UTC)
juan_gandhi: (Default)
From: [personal profile] juan_gandhi
У меня в cкале это выглядит так (мой класс Result куда старше Раста):

sealed trait Result[+T] extends Container[T] with Goodness:
...
type NoResult = Result[Nothing]
type Outcome = Result[Unit]

Date: 2025-02-27 22:17 (UTC)
juan_gandhi: (Default)
From: [personal profile] juan_gandhi
Yes, pretty cool.
Not monadic yet, not applicative; does not grab the stacktrace, does not record the timestamp, but otherwise yes, pretty close.

Here's my version.

Date: 2025-02-27 23:02 (UTC)
juan_gandhi: (Default)
From: [personal profile] juan_gandhi
Возможно.
Да похоже, не он один.
Когда я вижу в расте и в катсах эти ихние Results, я вспоминаю, как его задвигал лет этак 15 назад.

Ну и чо, и пусть.

Date: 2025-03-01 17:18 (UTC)
lomeo: (Default)
From: [personal profile] lomeo
У вас разные Result. Rust-овский - это Either, где второй параметр тип ошибки.

Date: 2025-03-01 21:54 (UTC)
juan_gandhi: (Default)
From: [personal profile] juan_gandhi
Ну то есть он не монадичен и не аппликативен. А мог бы.

Date: 2025-03-02 01:01 (UTC)
lomeo: (Default)
From: [personal profile] lomeo
Сложно говорить о монадичности в языке, который не поддерживает higher kind polymorphism. Но если смотреть не на типы, а на поведение, то вполне монадичен:
fn f(x: i32) -> Result<i32, &'static str> {
    Ok(x + 1)
}

fn g(_x: i32) -> Result<i32, &'static str> {
    Err("Oops")
}

fn h(x: i32) -> Result<i32, &'static str> {
    Ok(x * 2)
}

fn main() {
    let ok = Ok::<i32, &'static str>(42);
    let oops = Err::<i32, &'static str>("Oops");

    // left identity
    assert_eq!(Ok(42).and_then(f), f(42));
    assert_eq!(Ok(42).and_then(g), g(42));

    // right identity
    assert_eq!(ok.and_then(Ok), ok);
    assert_eq!(oops.and_then(Ok), oops);
    
    // associativity
    assert_eq!(
        ok.and_then(f).and_then(g),
        ok.and_then(|y| f(y).and_then(g)));
    assert_eq!(
        ok.and_then(f).and_then(h),
        ok.and_then(|y| f(y).and_then(h)));
    assert_eq!(
        oops.and_then(f).and_then(g),
        oops.and_then(|y| f(y).and_then(g)));
    assert_eq!(
        oops.and_then(f).and_then(h),
        oops.and_then(|y| f(y).and_then(h)));
}
Понятна, что жопу исключаем. Но мы её и в скале исключаем. Или что вы имели в виду?
Edited Date: 2025-03-02 01:03 (UTC)

Date: 2025-03-02 06:54 (UTC)
juan_gandhi: (Default)
From: [personal profile] juan_gandhi
Ах там есть and_then! Ну тогда да, монада. Спасибо. Чтобы апликативность была, нужно and_also. B z не имел в виду монадичности по параметру "ошибка", хотя, в принципе, можно было бы.

Cпасибо за разъяснения!

Date: 2025-03-02 16:05 (UTC)
lomeo: (Default)
From: [personal profile] lomeo
Возможно, что мы по разному смотрим на это. Для меня монада/функтор/аппликативный функтор - это не сам тип (хотя, мы, безусловно, говорим: X - монада), а операции (неважно существующие или потенциальные) над этим типом.

Т.е., когда вы говорите, X - не монада, то для меня это звучит как "невозможно построить операции над типом X, удовлетворяющие монадическим законам".

Построить `and_also` над растовым Result можно, поэтому (с моей точки зрения, конечно) Result является аппликативным функтором.

Извиняюсь за недопонимание :)

Date: 2025-03-02 16:21 (UTC)
juan_gandhi: (Default)
From: [personal profile] juan_gandhi
Ну в общем-то я тоже так это вижу. Добавить в Result метод, и будет счастье. Я не знаю, в расте есть extention methods? В скале-то нефиг делать к строкам понаприделывать практических полезных фигулек.

  implicit class powerString(s: String) {
    def containsIgnoreCase(other: String)   = s.toUpperCase.contains (other.toUpperCase)
    def strictContainsIgnoreCase(t: String) = !t.isEmpty && containsIgnoreCase (t)
    def | (alt: String) = if (s.isEmpty) alt else s
    def js = {
      require(s != null, "Illegal value: null")
      val s1 = s.trim
      val src = if (s1.startsWith("'") && s1.endsWith("'")) s1.tail.dropRight(1) else s1
      "'" + src.replaceAll("\\\\","\\\\\\\\").replaceAll("\'", "\\\\'") + "'"
    }
    def decodeHex: Array[Byte] = { // credits: Tomoya Yamamoto, https://gist.github.com/tmyymmt/3727124
      s.replaceAll("[^0-9A-Fa-f]", "").sliding(2, 2).toArray.map(Integer.parseInt(_, 16).toByte)
    }

    def quote = {
      val r = "\"" + s.replaceAll("\\\"", "\\\\\"").replaceAll("\n", "\\n") + "\""
      r
    }
  }

Date: 2025-03-03 04:43 (UTC)
lomeo: (Default)
From: [personal profile] lomeo
В rust-е это сделано сильно лучше скалы. См. impl, если вне твоего crate, то можно нарисовать trait, и сделать impl для своей структуры. Как будто вы имлементируете интерфейс отдельно от класса, типа instance в haskell, в scala это невозможно:
trait Extract32 {
    fn get_i32(&self, defval: i32) -> i32;
}

impl Extract32 for Result<i32, &'static str> {
    fn get_i32(&self, defval: i32) -> i32 {
        match self {
            Ok(v) => *v,
            _ => defval,
        }
    }
}

// и потом используем:

println!("{}", Ok(42).get_i32(-1));

Мало того, можно расширять все структуры имплементирующие какой-то интерфейс дополнительными методами.

У вас, кстати, extension methods по старому. В третьей есть extension (s: String) и вперёд!
Edited Date: 2025-03-03 04:54 (UTC)

Date: 2025-03-03 10:58 (UTC)
juan_gandhi: (Default)
From: [personal profile] juan_gandhi
Это ж по-скальски звучит как
  implicit class ExtraShit(Result[Int,String]) extends Extract32:
    def getInt(defVal: Int) = getOrElse defVal


Но да, с равенством будет фигня полная. Надо доопределять, как и hashCode. A implicit case class не бывает.

Extension - это в третьей скале, а у меня тут чота вторая. Надо будет проапгрейдить, давно собирался.



Edited Date: 2025-03-03 11:05 (UTC)

Date: 2025-03-03 04:50 (UTC)
lomeo: (Default)
From: [personal profile] lomeo
Поясню, почему я говорю "лучше". Если у меня есть структура и есть тип, то я хочу в произвольном месте объявлять эту структуру (или специализацию этой структуры, если она generic) этим типом. На примере scala:

// в одном месте
class A

// в другом месте
trait I { ... }

// невозможный синтаксис, вообще в третьем месте
// после которого класс A везде в нашем коде воспринимается как тип I
// как если бы он его имплементировал изначально
implementation of I for A { ... }


Extension methods эту проблему не решают, т.к. это просто сахар: либо обёрточный класс с нужными методами, либо статические методы.

В Scala можно делать implicit conversions, но это именно conversions, а не начальный класс, поэтому многое будет падать, например, equality.

Ограничения JVM, я так понимаю.

Date: 2025-03-03 10:29 (UTC)
juan_gandhi: (Default)
From: [personal profile] juan_gandhi
Ха, это верно насчёт equals.

scala> object Z:
     |  implicit class ZZ(s: String):
     |    def wrap: String = s"<<$s>>"
     | 
// defined object Z
                                                                                                                                     
scala> import Z._
                                                                                                                                     
scala> val z = ZZ("hello")
val z: Z.ZZ = Z$ZZ@12d44a2a
                                                                                                                                     
scala> z.equals(ZZ("hello"))
val res0: Boolean = false


Не case class, и не делегирует равенство.
Edited Date: 2025-03-03 11:06 (UTC)