Trabalhando com Option em Rust

Posted by Vanius on 15 October 2020

Diferente de muitas linguagens, Optional type é um mecanismo fundamental no desenvolvimento com Rust - ele está por trás da sua característica null safe. Mas além de funcionar como um Optional pointer, o tipo Option possui alguns recursos bem úteis. Option é um enumerado, em Rust isso significa que temos o poder dos enumerados algébricos, que permite levar consigo um determinado valor. Option é um enumerado de variedade None e Some(T). Muitas de suas operações possuem formato funcional - que faz um callback através de closure, sendo bastante eficiente, pois é invocado somente quando necessário. Vamos explorar isso e muito mais!

Obtendo o valor

Para obter o valor interno é possível utilizar as conhecidas formas if let e match. A diferença principal é que match é exaustivo, ou seja, devemos tratar as duas possibilidades do enumerado Option. Exemplo:

    
    let opt_some_1 = Some(1);

    if let Some(i) = opt_some_1 {
        println!("{}", i);
    }

    match opt_some_1 {
        Some(i) => println!("{}", i),
        None => println!("None"),
    }

Para saber apenas se o Option tem ou não tem conteúdo podemos usar is_some e is_none:


    let opt_some = Some(1);
    assert_eq!(opt_some.is_some(), true);

    let opt_none = None::<i32>;
    assert_eq!(opt_none.is_none(), true);
    

Podemos querer obter o valor, porém informando um default caso não existir. Para isso usamos o unwrap_or e unwrap_or_else, este último é o formato funcional. Exemplo:


    let opt_none : Option<i32> = None;

    let result = opt_none.unwrap_or(1);

    let result = opt_none.unwrap_or_else(|| 1);

Existe também o get_or_insert_with que parece funcionar como unwrap_or_else, porém ele atualiza o conteúdo do objeto, por isso necessita ser mutável (mut). O grande benefício desse método é que podemos usar como um artifício de lazy load - que carrega apenas uma vez e somente no momento do uso, se é que vai ser usado. Exemplo:


    let opt_none : Option<i32> = None;

    let result = opt_none.get_or_insert_with(|| 1);

Também podemos obter o valor através de unwrap, porém se Option possuir None causará um panic. Então devemos ter muita atenção para utilizá-lo.

Você pode imaginar Option sendo um iterator!

Inicialmente pode parecer estranho, mas tente imaginar Option sendo um iterator com tamanho que varia de 0 a 1. Isso significa que podemos fazer algumas operações idênticas que usamos com Vec, por exemplo.

Usando for:


    let opt_some_1 = Some(1);

    for i in opt_some_1 {
        println!("for opt_some_1: {:?}", i);
    }

Também é possível fazer operações através de formato funcional.

Vamos supor que queremos manter o valor, caso possuir, dada uma condição. Para isso podemos usar o filter:


    let opt_some_1 = Some(1);
    
    let filtered_opt = opt_some_1.filter(|i| i % 2 == 0);
    assert_eq!(filtered_opt, None);

Para fazer operação de map, que gera um novo Option com valor modificado, caso existir:


    let opt_some_1 = Some(1);
    let opt_map = opt_some_1.map(|i| i * 2);
    assert_eq!(opt_map, Some(2));

Também é possível mapear a operação e fornecer um valor quando não existe, através de map_or. Nesse caso sempre retorna um valor:


    let opt_some_1 = Some(1);
    let opt_map_or = opt_some_1.map_or(4, |i| i * 2);
    assert_eq!(opt_map, Some(2));

Option também pode retornar iter, into_iter e iter_mut:


    let opt_some_1 = Some(1);

    let found_1 = opt_some_1.iter().any(|i| i == &1);
    
    assert_eq!(found_1, true);

Encadeando condições

Podemos encadear sucessivas tentativas de obter o valor com outros Options, funcionando como um circuito lógico.

Aqui vamos considerar o OptionA o Option que está sendo chamado o método e OptionB o Option que está sendo passado no parâmetro.

O método and retorna OptionB, se o OptionA e OptionB forem Some(T).

O método or retorna OptionA (preferencialmente) ou OptionB, se um deles for Some(T).

Exemplo:


    let option_a = Some(1);

    let option_b = Some(2);

    let option_c = Some(3);

    let result_and = option_a.and(option_b).and(option_c);
    assert_eq!(result_and, Some(3));

    let result_and = option_a.or(option_b).or(option_c);
    assert_eq!(result_and, Some(1));

Suas variantes funcionais são and_then e or_else.

Pelo fato do and_then pegar o conteúdo do valor e permitir modificar, pode parecer muito semelhante ao map. A diferença é que em and_then a função parâmetro deve retornar um Option<T> e em map deve retornar um T. Dizemos que and_then invoca uma função falível, pois pode retornar None.

Uma boa utilidade do or_else, por exemplo, é quando tempos muitas maneiras de obter um valor. Vamos supor que estamos desenvolvendo um programa que pode obter uma parametrização via linha de comando, variável de ambiente ou arquivo de configuração. Com o or_else podemos encandear várias tentativas.

Trocando o owner de objetos

Um método muito útil é o take. Ele retorna um novo Option e coloca um None lugar. Exemplo:


    let mut opt_src = Some(1);

    let opt_dst = opt_src.take();

    assert_eq!(opt_src, None);
    assert_eq!(opt_dst, Some(1));

Uma grande utilidade do take é poder transferir um objeto para o outro, trocando o seu dono. Como no exemplo:


    #[derive(PartialEq, Debug)]
    struct Data {
        content: String,
    }

    impl Data {
        fn new(content: String) -> Data {
            Data { content }
        }
    }

    struct Container {
        data: Option<Data>,
    }

    impl Container {
        fn new(data: Option<Data>) -> Container {
            Container { data }
        }

        fn get_data(&mut self) -> Option<Data> {
            self.data.take()
        }
    }

    fn main() {
        let data = Data::new("Content".to_string());

        let mut container_a = Container::new(Some(data));

        let mut container_b = Container::new(container_a.get_data());

        let container_c = Container::new(container_b.get_data());

        println!("{:?}", container_c.data);
    }

Objeto data é passado para container_a, container_b e container_c sem nenhuma operação de clonagem.

Conclusão

Option de Rust é um tipo bastante elegante e moderno. Vimos várias maneiras interessante de manipular Option. Como é um tipo bastante usado, é altamente recomendado dominar alguns desses métodos, certamente nos tornaremos um programador mais capacitado.