이번 편은 코드만 나열되고 새로 배울 건 없다.

오늘은 아래와 같은 파이썬 코드와 비슷한 코드를 러스트에서도 보는 날이다.


a = sum(range(100)[1::2][:12])
print(a)
b = "sum(range(100)[1::2][:12])"[4:-6][::-1]
print(b)


144 ]2::1[)001(egnar


기대가 된다. 위 같은 코드가 과연 러스트에서는 어떤 꼴이 될까?


"sum(range(100)[1::2][:12])"[4:-6][::-1]
/* 가장 예쁘지만 불가능. Rust의 Range expressions을 사용해
(start..stop, step) 꼴을 인자로 받도록 만들어 보자. */

"sum(range(100)[1::2][:12])"[(4..-6,)][(.., -1)]
/* 불가능하다. Index는 self consume하지 않고,
&self와 같은 수명의 참조자만 반환할 수 있다. */

"sum(range(100)[1::2][:12])".pipe() | (4..-6) | (.., -1)
/* c++20의 view pipeline을 닮은 형식은 예쁘지만
연산자 오버로딩에서 orphan rule을 피하려면 귀찮게 새 타입을 만들어야 한다. */

"sum(range(100)[1::2][:12])".slice(4..-6).slice((.., -1))
/* 자바스크립트 형식은 예쁘다. 이걸로 하자. */


마지막 꼴을 만들기 위해 새로운 반복자 타입 Slice<T>을 만든다. 이 타입은 start, stop, step을 받아서 알맞게 아이템을 주는 역할을 한다. Slice<T>의 생성자는 아래 모습처럼 skip, take, step_by를 활용해서 만들 것이다. 그럼 아래 코드처럼 나올 것이다.


#[derive(Clone, Debug)]
pub struct Slice<T> {
iter: ??????
}
impl<T> Slice<T> where
T : DoubleEndedIterator + ExactSizeIterator
{
pub fn new(mut iter: T, start: Option<isize>, stop: Option<isize>, step: Option<isize>) -> Self {
// 구현 생략
let new_iter;
if step > 0 {
new_iter = iter.take(take_n).skip(skip_n).step_by(step as usize);
} else {
new_iter = iter.rev().take(take_n).skip(skip_n).step_by(-step as usize);
}
Slice{iter: new_iter}
}
}


위 코드는 문제가 있다. new_iter의 타입 될 수 있는 iter.take(take_n).skip(skip_n).step_by(step as usize)iter.rev().take(take_n).skip(skip_n).step_by(step as usize)의 타입이 다르다. 전자는 StepBy<Skip<Take<T>>> 이고 후자는 StepBy<Skip<Take<Rev<T>>>>이기 때문이다. 이 두 타입을 동시에 다루기 위해서 enum 으로 EitherIter 타입을 만들어주자.


// use std::iter::{Iterator, DoubleEndedIterator, ExactSizeIterator}; #[derive(Clone, Debug)] pub enum EitherIter<T, U> { A(T), B(U) } impl<T, U, V> Iterator for EitherIter<T, U> where T : Iterator<Item=V>, U : Iterator<Item=V> { type Item = V; #[inline] fn next(&mut self) -> Option<Self::Item> { self.nth(0) } #[inline] fn size_hint(&self) -> (usize, Option<usize>) { match self { Self::A(ref a) => a.size_hint(), Self::B(ref b) => b.size_hint() } } #[inline] fn nth(&mut self, n: usize) -> Option<Self::Item> { match self { Self::A(ref mut a) => a.nth(n), Self::B(ref mut b) => b.nth(n) } } } impl<T, U, V> DoubleEndedIterator for EitherIter<T, U> where T : Iterator<Item=V> + DoubleEndedIterator, U : Iterator<Item=V> + DoubleEndedIterator { #[inline] fn next_back(&mut self) -> Option<Self::Item> { self.nth_back(0) } #[inline] fn nth_back(&mut self, n: usize) -> Option<Self::Item> { match self { Self::A(ref mut a) => a.nth_back(n), Self::B(ref mut b) => b.nth_back(n) } } } impl<T, U, V> ExactSizeIterator for EitherIter<T, U> where T : Iterator<Item=V> + ExactSizeIterator, U : Iterator<Item=V> + ExactSizeIterator {}


이제 이것을 이용해서 Slice<T>를 구현하면 아래와 같다.


use std::iter::{Take, Skip, StepBy, Rev};

#[derive(Clone, Debug)]
pub struct Slice<T> {
iter: EitherIter<StepBy<Skip<Take<T>>>, StepBy<Skip<Take<Rev<T>>>>>
}
impl<T> Slice<T> where
T : DoubleEndedIterator + ExactSizeIterator
{
pub fn new(mut iter: T, start: Option<isize>, stop: Option<isize>, step: Option<isize>) -> Self {
assert!(step != Some(0), "step cannot be 0!");
let len = iter.len() as isize;
let step = step.unwrap_or(1);
// new slice is lazy
use std::cmp::max;
let new_iter;
if step > 0 {
let skip_n = start.map(|s| if s > 0 {s} else {max(0, s+len)}).unwrap_or(0) as usize;
let take_n = stop.map(|s| if s > 0 {s} else {max(0, s+len)}).unwrap_or(len) as usize;
new_iter = EitherIter::A( iter.take(take_n).skip(skip_n).step_by(step as usize) );
} else {
let skip_n = start.map(|s| if s > 0 {max(0, len-s-1)} else {-s-1}).unwrap_or(0) as usize;
let take_n = stop.map(|s| if s > 0 {max(0, len-s-1)} else {-s-1}).unwrap_or(len) as usize;
new_iter = EitherIter::B( iter.rev().take(take_n).skip(skip_n).step_by(-step as usize) );
}
Slice{iter: new_iter}
}
}
impl<T> Iterator for Slice<T> where
T : DoubleEndedIterator + ExactSizeIterator
{
type Item = <T as Iterator>::Item;
#[inline]
fn next(&mut self) -> Option<Self::Item> {
self.iter.next()
}
#[inline]
fn size_hint(&self) -> (usize, Option<usize>) {
self.iter.size_hint()
}
#[inline]
fn nth(&mut self, n: usize) -> Option<Self::Item> {
self.iter.nth(n)
}
}
impl<T> DoubleEndedIterator for Slice<T> where
T : DoubleEndedIterator + ExactSizeIterator
{
#[inline]
fn next_back(&mut self) -> Option<Self::Item> {
self.iter.next_back()
}
#[inline]
fn nth_back(&mut self, n: usize) -> Option<Self::Item> {
self.iter.nth_back(n)
}
}
impl<T> ExactSizeIterator for Slice<T> where T : DoubleEndedIterator + ExactSizeIterator {}


이제 반복자 뒤에 .slice(1..4) 처럼 붙일 수 있게 트레잇을 만든다. 이때 저번에 다룬 트레잇을 이용한 타입 오버로딩이 사용된다.


pub trait IterIntoSlice<A> : DoubleEndedIterator + ExactSizeIterator + Sized { fn slice(self, arg: A) -> Slice<Self>; } impl<A> IterIntoSlice<(Range<isize>,)> for A where A : DoubleEndedIterator + ExactSizeIterator + Sized { #[inline] fn slice(self, arg: (Range<isize>,)) -> Slice<Self> { Slice::new(self, arg.0.start.into(), arg.0.end.into(), None) } } impl<A> IterIntoSlice<Range<isize>> for A where A : DoubleEndedIterator + ExactSizeIterator + Sized { #[inline] fn slice(self, arg: Range<isize>,) -> Slice<Self> { self.slice((arg,)) } } impl<A> IterIntoSlice<(RangeFrom<isize>,)> for A where A : DoubleEndedIterator + ExactSizeIterator + Sized { #[inline] fn slice(self, arg: (RangeFrom<isize>,)) -> Slice<Self> { Slice::new(self, arg.0.start.into(), None, None) } } impl<A> IterIntoSlice<RangeFrom<isize>> for A where A : DoubleEndedIterator + ExactSizeIterator + Sized { #[inline] fn slice(self, arg: RangeFrom<isize>) -> Slice<Self> { self.slice((arg,)) } } impl<A> IterIntoSlice<(RangeFull,)> for A where A : DoubleEndedIterator + ExactSizeIterator + Sized { #[inline] fn slice(self, _arg: (RangeFull,)) -> Slice<Self> { Slice::new(self, None, None, None) } } impl<A> IterIntoSlice<RangeFull> for A where A : DoubleEndedIterator + ExactSizeIterator + Sized { #[inline] fn slice(self, arg: RangeFull) -> Slice<Self> { self.slice((arg,)) } } impl<A> IterIntoSlice<(RangeTo<isize>,)> for A where A : DoubleEndedIterator + ExactSizeIterator + Sized { #[inline] fn slice(self, arg: (RangeTo<isize>,)) -> Slice<Self> { Slice::new(self, None, arg.0.end.into(), None) } } impl<A> IterIntoSlice<RangeTo<isize>> for A where A : DoubleEndedIterator + ExactSizeIterator + Sized { #[inline] fn slice(self, arg: RangeTo<isize>) -> Slice<Self> { self.slice((arg,)) } } impl<A> IterIntoSlice<(Range<isize>, isize)> for A where A : DoubleEndedIterator + ExactSizeIterator + Sized { #[inline] fn slice(self, arg: (Range<isize>, isize)) -> Slice<Self> { Slice::new(self, arg.0.start.into(), arg.0.end.into(), arg.1.into()) } } impl<A> IterIntoSlice<(RangeFrom<isize>, isize)> for A where A : DoubleEndedIterator + ExactSizeIterator + Sized { #[inline] fn slice(self, arg: (RangeFrom<isize>, isize)) -> Slice<Self> { Slice::new(self, arg.0.start.into(), None, arg.1.into()) } } impl<A> IterIntoSlice<(RangeFull, isize)> for A where A : DoubleEndedIterator + ExactSizeIterator + Sized { #[inline] fn slice(self, arg: (RangeFull, isize)) -> Slice<Self> { Slice::new(self, None, None, arg.1.into()) } } impl<A> IterIntoSlice<(RangeTo<isize>, isize)> for A where A : DoubleEndedIterator + ExactSizeIterator + Sized { #[inline] fn slice(self, arg: (RangeTo<isize>, isize)) -> Slice<Self> { Slice::new(self, None, arg.0.end.into(), arg.1.into()) } }


슬라이스 [T] 뒤에나 문자열 슬라이스 str 뒤에도 붙을 수 있도록 트레잇을 만들면 아래 코드가 된다.


pub trait IntoSlice<'a> {
type Input;
fn slice<A>(&'a self, arg: A) -> Slice<Self::Input> where
Self::Input : IterIntoSlice<A>;
}

impl<'a, T: 'a> IntoSlice<'a> for [T] {
type Input = Iter<'a, T>;
fn slice<A>(&'a self, arg: A) -> Slice<Self::Input> where
Self::Input : IterIntoSlice<A>
{
self.iter().slice(arg)
}
}
impl<'a> IntoSlice<'a> for str
{
type Input = Exact<Chars<'a>>;
fn slice<A>(&'a self, arg: A) -> Slice<Self::Input> where
Self::Input : IterIntoSlice<A>
{
self.chars().exact().slice(arg)
}
}


마지막으로 오버로딩편에서 만들었던 range함수도 DoubleEndedIterator+ExactSizeIterator 를 가질 수 있도록 새로 만들어주자.


struct RangeArg(isize, isize, isize);

impl From<isize> for RangeArg { fn from(arg: isize) -> Self { RangeArg(0, arg, 1) } } impl From<(isize,)> for RangeArg { fn from(arg: (isize,)) -> Self { RangeArg(0, arg.0, 1) } } impl From<(isize,isize)> for RangeArg { fn from(arg: (isize,isize)) -> Self { RangeArg(arg.0, arg.1, 1) } } impl From<(isize,isize,isize)> for RangeArg { fn from(arg: (isize,isize,isize)) -> Self { RangeArg(arg.0, arg.1, arg.2) } } use std::{ops::Range, iter::StepBy}; pub fn range<A: Into<RangeArg>>(arg: A) -> EitherIter<StepBy<Range<isize>>, StepBy<Rev<Range<isize>>>> { let RangeArg(start, stop, step) = arg.into(); assert!(step != 0, "step cannot be 0!"); if step > 0 { EitherIter::A( (start..stop).step_by(step as usize) ) } else { EitherIter::B( (stop+(match (start-stop)%-step {0=>-step, i=>i})..start+1).rev().step_by(-step as usize) ) } }


이제 모든 것이 준비되었다. 처음 본 파이썬 코드처럼 러스트 코드를 작성해보자.


fn main() {
let a = range(100).slice((1.., 2)).slice(..12).sum::<isize>();
println!("{a}");
let b = "sum(range(100)[1::2][:12])".slice(4..-6).slice((.., -1)).collect::<String>();
println!("{b}");
}


144 ]2::1[)001(egnar


이렇게 러스트는 더이상 파이썬이 부럽지 않다. 모든 문제는 해결되었다.