Implementazione e scenari di tipo funzionale OptionT

3

Hai mai implementato Option<T> di tipo funzionale? È discusso qui: link

Fondamentalmente si tratta di usare IEnumerable<T> con no o solo un elemento invece di riferimento a oggetti potenzialmente nullable in C #. Possiamo potenziare la funzionalità LINQ per semplificare l'elaborazione riducendo la complessità ciclomatica a causa della mancanza di condizioni "if (null)".

La mia adozione dell'idea sembra in questo modo:

public struct Optional<T> : IEnumerable<T>
    where T : class
{
    public static implicit operator Optional<T>(T value)
    {
        return new Optional<T>(value);
    }

    public static implicit operator T(Optional<T> optional)
    {
        return optional.Value;
    }

    Optional(T value)
        : this()
    {
        Value = value;
    }

    public IEnumerator<T> GetEnumerator()
    {
        if (HasValue)
            yield return Value;
    }

    T Value { get; }
    bool HasValue => Value != null;
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
    public override string ToString() => Value?.ToString();
}

Non è così male, possiamo usarlo come argomento / tipo di ritorno con conversione automatica da / a T quando necessario. L'esempio del consumo potrebbe essere simile a questo:

     var p = new Product("Milk");
     var basket = ShoppingBasket.Empty
            .Place(p)
            .Place(p)
            .Place(null);

Crea un cestino con una singola entrata nel carrello per due scatole del latte se è definito quanto segue:

public static class ShoppingBasket
{
    public static readonly IEnumerable<BasketItem> Empty =
        Enumerable.Empty<BasketItem>();

    public static IEnumerable<BasketItem> Place(
        this IEnumerable<BasketItem> basket, 
        Optional<Product> product) =>
            basket
                .SetOrAdd(
                    i => product.Contains(i.Product), 
                    i => i.OneMore(), 
                    product.Select(p => new BasketItem(p, 1)))
                .ToArray();  
}

Dove:

public class BasketItem
{
    public BasketItem(Product product, int quantity)
    {
        Product = product;
        Quantity = quantity;
    }

    public Product Product { get; }
    public int Quantity { get; }
    public BasketItem OneMore() => 
        new BasketItem(Product, Quantity + 1);
}

public class Product
{
    public Product(string name)
    {
        Name = name;
    }

    public string Name { get; }
}

Uso questa estensione generica IEnumerable<T> :

public static class EnumerableHelper
{
    public static IEnumerable<T> SetOrAdd<T>(
        this IEnumerable<T> source,
        Func<T, bool> predicate,
        Func<T, T> set,
        params T[] add)
    {
        return source
            .SetOrAdd(predicate, set, add as IEnumerable<T>);
    } 

    public static IEnumerable<T> SetOrAdd<T>(
        this IEnumerable<T> source,
        Func<T, bool> predicate,
        Func<T, T> set,
        IEnumerable<T> add)
    {
        var empty = Enumerable.Empty<T>();
        foreach (var item in source)
            if (predicate(item))
            {
                yield return set(item);
                add = empty;
            }
            else
                yield return item;

        foreach (var item in add)
            yield return item;
    }
}

Bene, è abbastanza funzionale per quanto posso vedere. Ma la domanda è: ha senso? La complessità ciclomatica è molto bassa nella classe ShoppingBasket. Ma allo stesso tempo abbiamo ancora tre "percorsi virtuali" di esecuzione da testare - per "aggiungere" e "sostituire" nel metodo Place () + abbiamo anche bisogno di testare l'argomento del prodotto nullo.

Il problema è che sono quasi invisibili. Che ne dici? Preferiresti personalmente mantenere questo tipo di codice in C #?

    
posta Dmitry Nogin 24.01.2016 - 14:48
fonte

0 risposte