extendr_api/robj/
into_robj.rs

1use super::*;
2use crate::scalar::Scalar;
3use crate::single_threaded;
4use extendr_ffi::{
5    cetype_t, R_BlankString, R_NaInt, R_NaReal, R_NaString, R_NilValue, Rcomplex, Rf_mkCharLenCE,
6    COMPLEX, INTEGER, LOGICAL, RAW, REAL, SET_STRING_ELT, SEXPTYPE,
7};
8mod repeat_into_robj;
9
10/// Returns an `CHARSXP` based on the provided `&str`.
11///
12/// Note that R does string interning, thus repeated application of this
13/// function on the same string, will incur little computational cost.
14///
15/// Note, that you must protect the return value somehow.
16pub(crate) fn str_to_character(s: &str) -> SEXP {
17    unsafe {
18        if s.is_na() {
19            R_NaString
20        } else if s.is_empty() {
21            R_BlankString
22        } else {
23            single_threaded(|| {
24                // this function embeds a terminating \nul
25                Rf_mkCharLenCE(s.as_ptr().cast(), s.len() as i32, cetype_t::CE_UTF8)
26            })
27        }
28    }
29}
30
31/// Convert a null to an Robj.
32impl From<()> for Robj {
33    fn from(_: ()) -> Self {
34        // Note: we do not need to protect this.
35        unsafe { Robj::from_sexp(R_NilValue) }
36    }
37}
38
39/// Convert a [`Result`] to an [`Robj`].
40///
41/// Panics if there is an error.
42///
43/// To use the `?`-operator, an extendr-function must return either [`extendr_api::error::Result`] or [`std::result::Result`].
44/// Use of `panic!` in extendr is discouraged due to memory leakage.
45///
46/// Alternative behaviors enabled by feature toggles:
47/// extendr-api supports different conversions from [`Result<T,E>`] into `Robj`.
48/// Below, `x_ok` represents an R variable on R side which was returned from rust via `T::into_robj()` or similar.
49/// Likewise, `x_err` was returned to R side from rust via `E::into_robj()` or similar.
50/// extendr-api
51/// * `result_list`: `Ok(T)` is encoded as `list(ok = x_ok, err = NULL)` and `Err` as `list(ok = NULL, err = e_err)`.
52/// * `result_condition'`: `Ok(T)` is encoded as `x_ok` and `Err(E)` as `condition(msg="extendr_error", value = x_err, class=c("extendr_error", "error", "condition"))`
53/// * More than one enabled feature: Only one feature gate will take effect, the current order of precedence is [`result_list`, `result_condition`, ... ].
54/// * Neither of the above (default): `Ok(T)` is encoded as `x_ok`and `Err(E)` will trigger `throw_r_error()`, which is discouraged.
55/// ```
56/// use extendr_api::prelude::*;
57/// fn my_func() -> Result<f64> {
58///     Ok(1.0)
59/// }
60///
61/// test! {
62///     assert_eq!(r!(my_func()), r!(1.0));
63/// }
64/// ```
65///
66/// [`extendr_api::error::Result`]: crate::error::Result
67#[cfg(not(any(feature = "result_list", feature = "result_condition")))]
68impl<T, E> From<std::result::Result<T, E>> for Robj
69where
70    T: Into<Robj>,
71    E: std::fmt::Debug,
72{
73    fn from(res: std::result::Result<T, E>) -> Self {
74        res.unwrap().into()
75    }
76}
77
78/// Convert a [`Result`] to an [`Robj`]. Return either `Ok` value or `Err` value wrapped in an
79/// error condition. This allows using `?` operator in functions
80/// and returning [`Result<T>`] without panicking on `Err`. `T` must implement [`IntoRobj`].
81///
82/// Returns `Ok` value as is. Returns `Err` wrapped in an R error condition. The `Err` is placed in
83/// $value field of the condition, and its message is set to 'extendr_err'
84#[cfg(all(feature = "result_condition", not(feature = "result_list")))]
85impl<T, E> From<std::result::Result<T, E>> for Robj
86where
87    T: Into<Robj>,
88    E: Into<Robj>,
89{
90    fn from(res: std::result::Result<T, E>) -> Self {
91        use crate as extendr_api;
92        match res {
93            Ok(x) => x.into(),
94            Err(x) => {
95                let mut err = list!(message = "extendr_err", value = x.into());
96                err.set_class(["extendr_error", "error", "condition"])
97                    .expect("internal error: failed to set class");
98                err.into()
99            }
100        }
101    }
102}
103
104/// Convert a `Result` to an R `List` with an `ok` and `err` elements.
105/// This allows using `?` operator in functions
106/// and returning [`std::result::Result`] or [`extendr_api::error::Result`]
107/// without panicking on `Err`.
108///
109/// [`extendr_api::error::Result`]: crate::error::Result
110#[cfg(feature = "result_list")]
111impl<T, E> From<std::result::Result<T, E>> for Robj
112where
113    T: Into<Robj>,
114    E: Into<Robj>,
115{
116    fn from(res: std::result::Result<T, E>) -> Self {
117        use crate as extendr_api;
118        let mut result = match res {
119            Ok(x) => list!(ok = x.into(), err = NULL),
120            Err(x) => {
121                let err_robj = x.into();
122                if err_robj.is_null() {
123                    panic!("Internal error: result_list not allowed to return NULL as err-value")
124                }
125                list!(ok = NULL, err = err_robj)
126            }
127        };
128        result
129            .set_class(&["extendr_result"])
130            .expect("Internal error: failed to set class");
131        result.into()
132    }
133}
134
135// string conversions from Error trait to Robj and String
136impl From<Error> for Robj {
137    fn from(res: Error) -> Self {
138        res.to_string().into()
139    }
140}
141impl From<Error> for String {
142    fn from(res: Error) -> Self {
143        res.to_string()
144    }
145}
146
147/// Convert an Robj reference into a borrowed Robj.
148impl From<&Robj> for Robj {
149    // Note: we should probably have a much better reference
150    // mechanism as double-free or underprotection is a distinct possibility.
151    fn from(val: &Robj) -> Self {
152        unsafe { Robj::from_sexp(val.get()) }
153    }
154}
155
156/// This is an extension trait to provide a convenience method `into_robj()`.
157///
158/// Defer to `From<T> for Robj`-impls if you have custom types.
159///
160pub trait IntoRobj {
161    fn into_robj(self) -> Robj;
162}
163
164impl<T> IntoRobj for T
165where
166    Robj: From<T>,
167{
168    fn into_robj(self) -> Robj {
169        self.into()
170    }
171}
172
173/// `ToVectorValue` is a trait that allows many different types
174/// to be converted to vectors. It is used as a type parameter
175/// to `collect_robj()`.
176pub trait ToVectorValue {
177    fn sexptype() -> SEXPTYPE {
178        SEXPTYPE::NILSXP
179    }
180
181    fn to_real(&self) -> f64
182    where
183        Self: Sized,
184    {
185        0.
186    }
187
188    fn to_complex(&self) -> Rcomplex
189    where
190        Self: Sized,
191    {
192        Rcomplex { r: 0., i: 0. }
193    }
194
195    fn to_integer(&self) -> i32
196    where
197        Self: Sized,
198    {
199        std::i32::MIN
200    }
201
202    fn to_logical(&self) -> i32
203    where
204        Self: Sized,
205    {
206        std::i32::MIN
207    }
208
209    fn to_raw(&self) -> u8
210    where
211        Self: Sized,
212    {
213        0
214    }
215
216    fn to_sexp(&self) -> SEXP
217    where
218        Self: Sized,
219    {
220        unsafe { R_NilValue }
221    }
222}
223
224macro_rules! impl_real_tvv {
225    ($t: ty) => {
226        impl ToVectorValue for $t {
227            fn sexptype() -> SEXPTYPE {
228                SEXPTYPE::REALSXP
229            }
230
231            fn to_real(&self) -> f64 {
232                *self as f64
233            }
234        }
235
236        impl ToVectorValue for &$t {
237            fn sexptype() -> SEXPTYPE {
238                SEXPTYPE::REALSXP
239            }
240
241            fn to_real(&self) -> f64 {
242                **self as f64
243            }
244        }
245
246        impl ToVectorValue for Option<$t> {
247            fn sexptype() -> SEXPTYPE {
248                SEXPTYPE::REALSXP
249            }
250
251            fn to_real(&self) -> f64 {
252                if self.is_some() {
253                    self.unwrap() as f64
254                } else {
255                    unsafe { R_NaReal }
256                }
257            }
258        }
259    };
260}
261
262impl_real_tvv!(f64);
263impl_real_tvv!(f32);
264
265// Since these types might exceeds the max or min of R's 32bit integer, we need
266// to return as REALSXP
267impl_real_tvv!(i64);
268impl_real_tvv!(u32);
269impl_real_tvv!(u64);
270impl_real_tvv!(usize);
271
272macro_rules! impl_complex_tvv {
273    ($t: ty) => {
274        impl ToVectorValue for $t {
275            fn sexptype() -> SEXPTYPE {
276                SEXPTYPE::CPLXSXP
277            }
278
279            fn to_complex(&self) -> Rcomplex {
280                unsafe { std::mem::transmute(*self) }
281            }
282        }
283
284        impl ToVectorValue for &$t {
285            fn sexptype() -> SEXPTYPE {
286                SEXPTYPE::CPLXSXP
287            }
288
289            fn to_complex(&self) -> Rcomplex {
290                unsafe { std::mem::transmute(**self) }
291            }
292        }
293    };
294}
295
296impl_complex_tvv!(c64);
297impl_complex_tvv!(Rcplx);
298impl_complex_tvv!((f64, f64));
299
300macro_rules! impl_integer_tvv {
301    ($t: ty) => {
302        impl ToVectorValue for $t {
303            fn sexptype() -> SEXPTYPE {
304                SEXPTYPE::INTSXP
305            }
306
307            fn to_integer(&self) -> i32 {
308                *self as i32
309            }
310        }
311
312        impl ToVectorValue for &$t {
313            fn sexptype() -> SEXPTYPE {
314                SEXPTYPE::INTSXP
315            }
316
317            fn to_integer(&self) -> i32 {
318                **self as i32
319            }
320        }
321
322        impl ToVectorValue for Option<$t> {
323            fn sexptype() -> SEXPTYPE {
324                SEXPTYPE::INTSXP
325            }
326
327            fn to_integer(&self) -> i32 {
328                if self.is_some() {
329                    self.unwrap() as i32
330                } else {
331                    unsafe { R_NaInt }
332                }
333            }
334        }
335    };
336}
337
338impl_integer_tvv!(i8);
339impl_integer_tvv!(i16);
340impl_integer_tvv!(i32);
341impl_integer_tvv!(u16);
342
343impl ToVectorValue for u8 {
344    fn sexptype() -> SEXPTYPE {
345        SEXPTYPE::RAWSXP
346    }
347
348    fn to_raw(&self) -> u8 {
349        *self
350    }
351}
352
353impl ToVectorValue for &u8 {
354    fn sexptype() -> SEXPTYPE {
355        SEXPTYPE::RAWSXP
356    }
357
358    fn to_raw(&self) -> u8 {
359        **self
360    }
361}
362
363macro_rules! impl_str_tvv {
364    ($t: ty) => {
365        impl ToVectorValue for $t {
366            fn sexptype() -> SEXPTYPE {
367                SEXPTYPE::STRSXP
368            }
369
370            fn to_sexp(&self) -> SEXP
371            where
372                Self: Sized,
373            {
374                str_to_character(self.as_ref())
375            }
376        }
377
378        impl ToVectorValue for &$t {
379            fn sexptype() -> SEXPTYPE {
380                SEXPTYPE::STRSXP
381            }
382
383            fn to_sexp(&self) -> SEXP
384            where
385                Self: Sized,
386            {
387                str_to_character(self.as_ref())
388            }
389        }
390
391        impl ToVectorValue for Option<$t> {
392            fn sexptype() -> SEXPTYPE {
393                SEXPTYPE::STRSXP
394            }
395
396            fn to_sexp(&self) -> SEXP
397            where
398                Self: Sized,
399            {
400                if let Some(s) = self {
401                    str_to_character(s.as_ref())
402                } else {
403                    unsafe { R_NaString }
404                }
405            }
406        }
407    };
408}
409
410impl_str_tvv! {&str}
411impl_str_tvv! {String}
412
413impl ToVectorValue for bool {
414    fn sexptype() -> SEXPTYPE {
415        SEXPTYPE::LGLSXP
416    }
417
418    fn to_logical(&self) -> i32
419    where
420        Self: Sized,
421    {
422        *self as i32
423    }
424}
425
426impl ToVectorValue for &bool {
427    fn sexptype() -> SEXPTYPE {
428        SEXPTYPE::LGLSXP
429    }
430
431    fn to_logical(&self) -> i32
432    where
433        Self: Sized,
434    {
435        **self as i32
436    }
437}
438
439impl ToVectorValue for Rbool {
440    fn sexptype() -> SEXPTYPE {
441        SEXPTYPE::LGLSXP
442    }
443
444    fn to_logical(&self) -> i32
445    where
446        Self: Sized,
447    {
448        self.inner()
449    }
450}
451
452impl ToVectorValue for &Rbool {
453    fn sexptype() -> SEXPTYPE {
454        SEXPTYPE::LGLSXP
455    }
456
457    fn to_logical(&self) -> i32
458    where
459        Self: Sized,
460    {
461        self.inner()
462    }
463}
464
465impl ToVectorValue for Option<bool> {
466    fn sexptype() -> SEXPTYPE {
467        SEXPTYPE::LGLSXP
468    }
469
470    fn to_logical(&self) -> i32 {
471        if self.is_some() {
472            self.unwrap() as i32
473        } else {
474            unsafe { R_NaInt }
475        }
476    }
477}
478
479// Not thread safe.
480fn fixed_size_collect<I>(iter: I, len: usize) -> Robj
481where
482    I: Iterator,
483    I: Sized,
484    I::Item: ToVectorValue,
485{
486    single_threaded(|| unsafe {
487        // Length of the vector is known in advance.
488        let sexptype = I::Item::sexptype();
489        if sexptype != SEXPTYPE::NILSXP {
490            let res = Robj::alloc_vector(sexptype, len);
491            let sexp = res.get();
492            match sexptype {
493                SEXPTYPE::REALSXP => {
494                    let ptr = REAL(sexp);
495                    for (i, v) in iter.enumerate() {
496                        *ptr.add(i) = v.to_real();
497                    }
498                }
499                SEXPTYPE::CPLXSXP => {
500                    let ptr = COMPLEX(sexp);
501                    for (i, v) in iter.enumerate() {
502                        *ptr.add(i) = v.to_complex();
503                    }
504                }
505                SEXPTYPE::INTSXP => {
506                    let ptr = INTEGER(sexp);
507                    for (i, v) in iter.enumerate() {
508                        *ptr.add(i) = v.to_integer();
509                    }
510                }
511                SEXPTYPE::LGLSXP => {
512                    let ptr = LOGICAL(sexp);
513                    for (i, v) in iter.enumerate() {
514                        *ptr.add(i) = v.to_logical();
515                    }
516                }
517                SEXPTYPE::STRSXP => {
518                    for (i, v) in iter.enumerate() {
519                        SET_STRING_ELT(sexp, i as isize, v.to_sexp());
520                    }
521                }
522                SEXPTYPE::RAWSXP => {
523                    let ptr = RAW(sexp);
524                    for (i, v) in iter.enumerate() {
525                        *ptr.add(i) = v.to_raw();
526                    }
527                }
528                _ => {
529                    panic!("unexpected SEXPTYPE in collect_robj");
530                }
531            }
532            res
533        } else {
534            Robj::from(())
535        }
536    })
537}
538
539/// Extensions to iterators for R objects including [RobjItertools::collect_robj()].
540pub trait RobjItertools: Iterator {
541    /// Convert a wide range of iterators to Robj.
542    /// ```
543    /// use extendr_api::prelude::*;
544    ///
545    /// test! {
546    /// // Integer iterators.
547    /// let robj = (0..3).collect_robj();
548    /// assert_eq!(robj.as_integer_vector().unwrap(), vec![0, 1, 2]);
549    ///
550    /// // Logical iterators.
551    /// let robj = (0..3).map(|x| x % 2 == 0).collect_robj();
552    /// assert_eq!(robj.as_logical_vector().unwrap(), vec![TRUE, FALSE, TRUE]);
553    ///
554    /// // Numeric iterators.
555    /// let robj = (0..3).map(|x| x as f64).collect_robj();
556    /// assert_eq!(robj.as_real_vector().unwrap(), vec![0., 1., 2.]);
557    ///
558    /// // String iterators.
559    /// let robj = (0..3).map(|x| format!("{}", x)).collect_robj();
560    /// assert_eq!(robj.as_str_vector(), Some(vec!["0", "1", "2"]));
561    /// }
562    /// ```
563    fn collect_robj(self) -> Robj
564    where
565        Self: Iterator,
566        Self: Sized,
567        Self::Item: ToVectorValue,
568    {
569        if let (len, Some(max)) = self.size_hint() {
570            if len == max {
571                return fixed_size_collect(self, len);
572            }
573        }
574        // If the size is indeterminate, create a vector and call recursively.
575        let vec: Vec<_> = self.collect();
576        assert!(vec.iter().size_hint() == (vec.len(), Some(vec.len())));
577        vec.into_iter().collect_robj()
578    }
579
580    /// Collects an iterable into an [`RArray`].
581    /// The iterable must yield items column by column (aka Fortan order)
582    ///
583    /// # Arguments
584    ///
585    /// * `dims` - an array containing the length of each dimension
586    fn collect_rarray<const LEN: usize>(
587        self,
588        dims: [usize; LEN],
589    ) -> Result<RArray<Self::Item, [usize; LEN]>>
590    where
591        Self: Iterator,
592        Self: Sized,
593        Self::Item: ToVectorValue,
594        Robj: for<'a> AsTypedSlice<'a, Self::Item>,
595    {
596        let mut vector = self.collect_robj();
597        let prod = dims.iter().product::<usize>();
598        if prod != vector.len() {
599            return Err(Error::Other(format!(
600                "The vector length ({}) does not match the length implied by the dimensions ({})",
601                vector.len(),
602                prod
603            )));
604        }
605        vector.set_attrib(wrapper::symbol::dim_symbol(), dims.iter().collect_robj())?;
606        let _data = vector.as_typed_slice().ok_or(Error::Other(
607            "Unknown error in converting to slice".to_string(),
608        ))?;
609        Ok(RArray::from_parts(vector, dims))
610    }
611}
612
613// Thanks to *pretzelhammer* on stackoverflow for this.
614impl<T> RobjItertools for T where T: Iterator {}
615
616// Scalars which are ToVectorValue
617impl<T> From<T> for Robj
618where
619    T: ToVectorValue,
620{
621    fn from(scalar: T) -> Self {
622        Some(scalar).into_iter().collect_robj()
623    }
624}
625
626macro_rules! impl_from_as_iterator {
627    ($t: ty) => {
628        impl<T> From<$t> for Robj
629        where
630            $t: RobjItertools,
631            <$t as Iterator>::Item: ToVectorValue,
632            T: ToVectorValue,
633        {
634            fn from(val: $t) -> Self {
635                val.collect_robj()
636            }
637        }
638    };
639}
640
641// impl<T> From<Range<T>> for Robj
642// where
643//     Range<T> : RobjItertools,
644//     <Range<T> as Iterator>::Item: ToVectorValue,
645//     T : ToVectorValue
646// {
647//     fn from(val: Range<T>) -> Self {
648//         val.collect_robj()
649//     }
650// } //
651
652impl<'a, T, const N: usize> From<[T; N]> for Robj
653where
654    Self: 'a,
655    T: ToVectorValue,
656{
657    fn from(val: [T; N]) -> Self {
658        fixed_size_collect(val.into_iter(), N)
659    }
660}
661
662impl<'a, T, const N: usize> From<&'a [T; N]> for Robj
663where
664    Self: 'a,
665    &'a T: ToVectorValue + 'a,
666{
667    fn from(val: &'a [T; N]) -> Self {
668        fixed_size_collect(val.iter(), N)
669    }
670}
671
672impl<'a, T, const N: usize> From<&'a mut [T; N]> for Robj
673where
674    Self: 'a,
675    &'a mut T: ToVectorValue + 'a,
676{
677    fn from(val: &'a mut [T; N]) -> Self {
678        fixed_size_collect(val.iter_mut(), N)
679    }
680}
681
682impl<T: ToVectorValue + Clone> From<&Vec<T>> for Robj {
683    fn from(value: &Vec<T>) -> Self {
684        let len = value.len();
685        fixed_size_collect(value.iter().cloned(), len)
686    }
687}
688
689impl<T: ToVectorValue> From<Vec<T>> for Robj {
690    fn from(value: Vec<T>) -> Self {
691        let len = value.len();
692        fixed_size_collect(value.into_iter(), len)
693    }
694}
695
696impl<'a, T> From<&'a [T]> for Robj
697where
698    Self: 'a,
699    T: 'a,
700    &'a T: ToVectorValue,
701{
702    fn from(val: &'a [T]) -> Self {
703        val.iter().collect_robj()
704    }
705}
706
707impl_from_as_iterator! {Range<T>}
708impl_from_as_iterator! {RangeInclusive<T>}
709
710impl From<Vec<Robj>> for Robj {
711    /// Convert a vector of Robj into a list.
712    fn from(val: Vec<Robj>) -> Self {
713        List::from_values(val.iter()).into()
714    }
715}
716
717impl From<Vec<Rstr>> for Robj {
718    /// Convert a vector of Rstr into strings.
719    fn from(val: Vec<Rstr>) -> Self {
720        Strings::from_values(val).into()
721    }
722}
723
724#[cfg(test)]
725mod test {
726    use super::*;
727    use crate as extendr_api;
728
729    #[test]
730    fn test_vec_rint_to_robj() {
731        test! {
732            let int_vec = vec![3,4,0,-2];
733            let int_vec_robj: Robj = int_vec.clone().into();
734            // unsafe { extendr_ffi::Rf_PrintValue(int_vec_robj.get())}
735            assert_eq!(int_vec_robj.as_integer_slice().unwrap(), &int_vec);
736
737            let rint_vec = vec![Rint::new(3), Rint::new(4), Rint::new(0), Rint::new(-2)];
738            let rint_vec_robj: Robj = rint_vec.into();
739            // unsafe { extendr_ffi::Rf_PrintValue(rint_vec_robj.get())}
740            assert_eq!(rint_vec_robj.as_integer_slice().unwrap(), &int_vec);
741        }
742    }
743
744    #[test]
745    fn test_collect_rarray_matrix() {
746        test! {
747            // Check that collect_rarray works the same as R's matrix() function
748            let rmat = (1i32..=16).collect_rarray([4, 4]);
749            assert!(rmat.is_ok());
750            assert_eq!(Robj::from(rmat), R!("matrix(1:16, nrow=4)").unwrap());
751        }
752    }
753
754    #[test]
755    fn test_collect_rarray_tensor() {
756        test! {
757            // Check that collect_rarray works the same as R's array() function
758            let rmat = (1i32..=16).collect_rarray([2, 4, 2]);
759            assert!(rmat.is_ok());
760            assert_eq!(Robj::from(rmat), R!("array(1:16, dim=c(2, 4, 2))").unwrap());
761        }
762    }
763
764    #[test]
765    fn test_collect_rarray_matrix_failure() {
766        test! {
767            // Check that collect_rarray fails when given an invalid shape
768            let rmat = (1i32..=16).collect_rarray([3, 3]);
769            assert!(rmat.is_err());
770            let msg = rmat.unwrap_err().to_string();
771            assert!(msg.contains('9'));
772            assert!(msg.contains("dimension"));
773        }
774    }
775
776    #[test]
777    fn test_collect_tensor_failure() {
778        test! {
779            // Check that collect_rarray fails when given an invalid shape
780            let rmat = (1i32..=16).collect_rarray([3, 3, 3]);
781            assert!(rmat.is_err());
782            let msg = rmat.unwrap_err().to_string();
783            assert!(msg.contains("27"));
784            assert!(msg.contains("dimension"));
785        }
786    }
787
788    #[test]
789    #[cfg(all(feature = "result_condition", not(feature = "result_list")))]
790    fn test_result_condition() {
791        use crate::prelude::*;
792        fn my_err_f() -> std::result::Result<f64, f64> {
793            Err(42.0) // return err float
794        }
795
796        test! {
797                  assert_eq!(
798                    r!(my_err_f()),
799                    R!(
800        "structure(list(message = 'extendr_err',
801        value = 42.0), class = c('extendr_error', 'error', 'condition'))"
802                    ).unwrap()
803                );
804            }
805    }
806
807    #[test]
808    #[cfg(feature = "result_list")]
809    fn test_result_list() {
810        use crate::prelude::*;
811        fn my_err_f() -> std::result::Result<f64, String> {
812            Err("We have water in the engine room!".to_string())
813        }
814
815        fn my_ok_f() -> std::result::Result<f64, String> {
816            Ok(123.123)
817        }
818
819        test! {
820            assert_eq!(
821                r!(my_err_f()),
822                R!("x=list(ok=NULL, err='We have water in the engine room!')
823                    class(x)='extendr_result'
824                    x"
825                ).unwrap()
826            );
827            assert_eq!(
828                r!(my_ok_f()),
829                R!("x = list(ok=123.123, err=NULL)
830                    class(x)='extendr_result'
831                    x"
832                ).unwrap()
833            );
834        }
835    }
836}