6
votes

Swift: encapsulation paresseuse de chaînes de map, filter, flatMap

J'ai une liste d'animaux:

let animals = ["bear", "dog", "cat"]

typealias Transform = (String) -> [String]

let containsA: Transform = { $0.contains("a") ? [$0] : [] }
let plural:    Transform = { [$0 + "s"] }
let double:    Transform = { [$0, $0] }

extension Array where Element == String {
  func transform(_ transforms: [Transform]) -> AnySequence<String> {

    return AnySequence<String> { () -> AnyIterator<String> in
      var iterator = self
        .lazy
        .flatMap(transforms[0])
        .flatMap(transforms[1])
        .flatMap(transforms[2])
        .makeIterator()

      return AnyIterator {
        return iterator.next()
      }
    }
  }
}

let transformed = animals.transform([containsA, plural, double])

print(Array(transformed))

Et quelques moyens de transformer cette liste:

  switch transforms.count {
  case 1:
    var iterator = self
      .lazy
      .flatMap(transforms[0])
      .makeIterator()
    return AnyIterator {
      return iterator.next()
    }
  case 2:
    var iterator = self
      .lazy
      .flatMap(transforms[0])
      .flatMap(transforms[1])
      .makeIterator()
    return AnyIterator {
      return iterator.next()
    }
  case 3:
    var iterator = self
      .lazy
      .flatMap(transforms[0])
      .flatMap(transforms[1])
      .flatMap(transforms[2])
      .makeIterator()
    return AnyIterator {
      return iterator.next()
    }
  default:
    fatalError(" Too many transforms!")
  }

En passant, ces sont analogues respectivement aux filtres (sorties 0 ou 1 élément), map (exactement 1 élément) et flatmap (plus d'un élément) mais définis de manière uniforme afin qu'ils puissent être traités de manière cohérente.

Je veux pour créer un itérateur paresseux qui applique un tableau de ces transformations à la liste des animaux:

  var lazyCollection = self.lazy
  for transform in transforms {
    lazyCollection = lazyCollection.flatMap(transform) //Error
  }
  var iterator = lazyCollection.makeIterator()

ce qui signifie que je peux faire paresseusement:

        .flatMap(transforms[0])
        .flatMap(transforms[1])
        .flatMap(transforms[2])


5 commentaires

ne pouvez-vous pas envoyer le nombre d'itérations?


transforms.count?


Comme vous le dites, je connais le nombre de transformations en tant que transforms.count mais je ne vois pas comment l'utiliser pour faire une boucle d'opérations ".flatMap" car le type change à chaque itération.


Je peux voir, le problème est que ce type pour autant que je sache, "itérateur" ne peut pas prendre de limites car il doit implémenter directement comme vous l'avez fait, mais une solution de contournement peut être appliquée en envoyant un indicateur pour utiliser un autre itérateur ou quelque chose .


Je me demande comment changer le titre de la question en: "Swift: encapsulation paresseuse de chaînes de map, filter, flatMap" car je pense que cela décrirait bien mieux l'essence de cette question, mais je me demande si elle est considérée comme une mauvaise forme pour changer le titre d'une question?


3 Réponses :


7
votes

Vous pouvez appliquer les transformations de façon récursive si vous définissez la méthode sur le protocole Sequence (au lieu de Array ). De plus, la contrainte where Element == String n'est pas nécessaire si le paramètre de transformations est défini comme un tableau de (Element) -> [Element] .

extension Sequence {
    func transform(_ transforms: [(Element) -> [Element]]) -> AnySequence<Element> {
        if transforms.isEmpty {
            return AnySequence(self)
        } else {
            return lazy.flatMap(transforms[0]).transform(Array(transforms[1...]))
        }
    }
}


0 commentaires

4
votes

Une autre approche pour réaliser ce que vous voulez:

Modifier : j'ai essayé:

let animals = ["bear", "dog", "cat"]

typealias Transform<Element> = (Element) -> [Element]

let containsA: Transform<String> = { $0.contains("a") ? [$0] : [] }
let plural:    Transform<String> = { [$0 + "s"] }
let double:    Transform<String> = { [$0, $0] }

extension Sequence {
    func transform(_ transforms: [Transform<Element>]) -> AnySequence<Element> {
        return transforms.reduce(AnySequence(self)) {sequence, transform in
            AnySequence(sequence.lazy.flatMap(transform))
        }
    }
}

let transformed = animals.transform([containsA, plural, double])

print(Array(transformed))

Vous étiez très proche de votre objectif, si les deux types de la ligne Erreur étaient assignables, votre code aurait fonctionné.

Une petite modification:

var transformedSequence = transforms.reduce(AnySequence(self.lazy)) {sequence, transform in
    AnySequence(sequence.flatMap(transform))
}
var iterator = transformedSequence.makeIterator()

Ou vous pouvez utiliser réduire ici:

var lazySequence = AnySequence(self.lazy)
for transform in transforms {
    lazySequence = AnySequence(lazySequence.flatMap(transform))
}
var iterator = lazySequence.makeIterator()

Le code entier serait:

( EDIT Modifié pour inclure les suggestions de Martin R.)

var lazyCollection = self.lazy
for transform in transforms {
    lazyCollection = lazyCollection.flatMap(transform) //Error
}
var iterator = lazyCollection.makeIterator()


3 commentaires

Très agréable! - Remarques mineures: La valeur initiale de réduire peut être AnySequence (self) , sans le paresseux . En revanche, AnySequence (sequence.lazy.flatMap (transform)) éviterait la création de tableaux intermédiaires.


Belle suggestion, franchement j'avais oublié que flatMap sur Sequence génère un tableau intermédiaire. Merci beaucoup.


Merci beaucoup Martin et OOPer. Votre pensée combinée m'a donné une solution élégante. Très appréciée.



2
votes

Pourquoi ne pas intégrer pleinement cela dans le monde fonctionnel? Par exemple, utiliser des chaînes (dynamiques) d'appels de fonction, comme filter (containsA) | map (pluriel) | flatMap (double) .

Avec un peu de code générique réutilisable, nous pouvons réaliser de belles choses.

Commençons par promouvoir certaines opérations de séquence et de séquence paresseuse vers des fonctions libres:

let animals = ["bear", "dog", "cat"]
let newAnimals = lazy(animals) | filter(containsA) | map(plural) | flatMap(double)
print(Array(newAnimals)) // ["bears", "bears", "cats", "cats"]

Notez que les homologues des séquences paresseuses sont plus verbeux que le Séquence , mais cela est dû à la verbosité des méthodes LazySequenceProtocol .

Avec ce qui précède, nous pouvons créer des fonctions génériques qui reçoivent des tableaux et renvoient des tableaux, et ceci type de fonctions sont extrêmement adaptés pour le pipelining, définissons donc un opérateur de pipeline:

typealias Transform<T, U> = (T) -> U

let containsA: Transform<String, Bool> = { $0.contains("a") }
let plural:    Transform<String, String> = { $0 + "s" }
let double:    Transform<String, [String]> = { [$0, $0] }

Maintenant, tout ce dont nous avons besoin est de fournir quelque chose à ces fonctions, mais pour y parvenir, nous besoin d'un peu de peaufinage sur le type Transform :

func |<T, U>(_ arg: T, _ f: (T) -> U) -> U {
    return f(arg)
}

Avec tout ce qui précède en place, les choses deviennent faciles et claires:

func lazy<S: Sequence>(_ arr: S) -> LazySequence<S> {
    return arr.lazy
}

func filter<S: Sequence>(_ isIncluded: @escaping (S.Element) throws -> Bool) -> (S) throws -> [S.Element] {
    return { try $0.filter(isIncluded) }
}

func filter<L: LazySequenceProtocol>(_ isIncluded: @escaping (L.Elements.Element) -> Bool) -> (L) -> LazyFilterSequence<L.Elements> {
    return { $0.filter(isIncluded) }
}

func map<S: Sequence, T>(_ transform: @escaping (S.Element) throws -> T) -> (S) throws -> [T] {
    return { try $0.map(transform) }
}

func map<L: LazySequenceProtocol, T>(_ transform: @escaping (L.Elements.Element) -> T) -> (L) -> LazyMapSequence<L.Elements, T> {
    return { $0.map(transform) }
}

func flatMap<S: Sequence, T: Sequence>(_ transform: @escaping (S.Element) throws -> T) -> (S) throws -> [T.Element] {
    return { try $0.flatMap(transform) }
}

func flatMap<L: LazySequenceProtocol, S: Sequence>(_ transform: @escaping (L.Elements.Element) -> S) -> (L) -> LazySequence<FlattenSequence<LazyMapSequence<L.Elements, S>>> {
    return { $0.flatMap(transform) }
}


6 commentaires

Hou la la! Très sympa @Cristik. Quand j'ai commencé à y penser, je me suis demandé s'il y avait un moyen de créer un opérateur pour combiner filtre, carte, flatMap, mais je n'explore la pensée fonctionnelle que depuis quelques mois pour que mon cerveau ne puisse pas le comprendre - si génial pour voir vos méthodes. Une chose qui n'est pas claire pour moi ... comment pourrais-je accéder au résultat paresseusement? let opération = filtre (contientA) | map (pluriel) | flatMap (double) pour x en fonctionnement (animaux) {print (x)} n'est pas paresseux?


@Adahus bonne question, la paresse dans cette approche ne semble pas bien fonctionner, j'ai donc mis à jour la réponse et supprimé les appels paresseux car ils semblent aider avec presque rien. J'essaierai de revenir avec une solution vraiment "paresseuse" :)


Je me demande si les fonctions gratuites doivent passer AnySequence?


@Adahus en fait, j'ai pu résoudre le problème uniquement via des protocoles, j'ai mis à jour la réponse et maintenant si vous fournissez une séquence paresseuse comme point de départ, tout le pipeline sera paresseux.


Wow encore! C'est génial. Merci d'avoir pris le temps d'écrire ceci. Même si ce n'est pas beaucoup de lignes de code, cela représente un ensemble assez important de méthodes. Mon prochain défi est de réfléchir à la façon de permettre à un produit cartésien paresseux d'être ajouté au pipeline ...


Je viens de découvrir github.com/pointfreeco/swift-overture qui est un territoire similaire. (Je n'ai aucun lien avec Pointfree.)