Skip to content

Implement TaskSeq.length, allPairs, cache, cast, concat, findIndex, contains, exists, init, initInfinite, indexed etc #38

Closed
@abelbraaksma

Description

@abelbraaksma

While most of the above have trivial semantics and should sometimes take an Async overload (i.e., findIndexAsync and initAsync for the lambda), I'm not entirely sure of the usefulness of TaskSeq.cache (and we already have TaskSeq.toSeqCached), but perhaps for parity it should be included.

A note on TaskSeq.cast vs Seq.cast

While the docs on Seq.cast say that it is meant for a "loosely typed sequence", to be cast to a generically strongly typed sequence, it can be used just fine to cast an F# seq, because it also implements IEnumerable (i.e., the non-generic variant).

// this is fine
let x: seq<uint> = seq { 1 } |> Seq.cast

But F# also allows you to perform an illegal cast:

// this won't give any warnings, but will throw an exception
let x: seq<Guid> = seq { 1 } |> Seq.cast

Since IAsyncEnumerable<'T> only exists as a typed sequence, we should honor that and only allow valid casts. However, F# doesn't allow a constraint like 'T :> 'U, the rh-side must be a concrete type. This means that in the end, it'll effectively work the same way as for Seq.cast through boxing. Alternatively, if the type-relation is known ahead of time, users should probably just write TaskSeq.map (fun x -> x :> _) just as they would for seq<_>.

// this is fine
let x: seq<uint> = taskSeq { 1 } |> TaskSeq.cast
// this *should* raise a compile-time exception, but there's no way to enforce that
let x: seq<Guid> = taskSeq { 1 } |> TaskSeq.cast  // incompatible

For comparison, Linq.Enumerable.Cast works through the untyped Enumerable as well.


EDIT: to get better parity with Seq.cast and Linq.Enumerable.Cast, I decided to only allow it to work with untyped task sequences (that is: IAsyncEnumerable<obj>). I've updated the signature below. In addition, we'll be adding a box and unbox helper as well, the latter only for value types. If users want a reinterpret_cast style cast, they can use TaskSeq.box >> TaskSeq.cast. Due to the static way this is compiled, this won't add overhead.

TODO list:

Proposals of signatures:

module TaskSeq =
    val length: source: taskSeq<'T> -> Task<int>
    val lengthBy: predicate: ('T -> bool) -> source: taskSeq<'T> -> Task<int>
    val lengthByAsync: predicate: ('T -> #Task<bool>) -> source: taskSeq<'T> -> Task<int>

    val allPairs: source1: taskSeq<'T> -> source2: taskSeq<'U> -> taskSeq<'T * 'U>
    val indexed: source: taskSeq<'T> -> taskSeq<int * 'T>
    val cache: source: taskSeq<'T> -> taskSeq<'T> // later, see MBP approach of AsyncSeq
    val cast: source: taskSeq<obj> -> taskSeq<'U>
    val box: source: taskSeq<'T> -> taskSeq<obj>
    val unbox<'T when 'T: struct> : source: taskSeq<obj> -> taskSeq<'U>
    val concat: source1: taskSeq<'T> -> source2: taskSeq<'T> -> taskSeq<'T>

    val findIndex: predicate: ('T -> bool) -> source: taskSeq<'T> -> Task<int>
    val findIndexAsync: predicate: ('T -> Task<bool>) -> source: taskSeq<'T> -> Task<int>
    val tryFindIndex: predicate: ('T -> bool) -> source: taskSeq<'T> -> Task<int option>
    val tryFindIndexAsync: predicate: ('T -> Task<bool>) -> source: taskSeq<'T> -> Task<int option>
    val contains: value: 'T -> source: taskSeq<'T> -> Task<bool> (requires comparison)
    val exists: predicate: ('T -> bool) -> source: taskSeq<'T> -> Task<bool>
    val existsAsync: predicate: ('T -> Task<bool>) -> source: taskSeq<'T> -> Task<bool>

    val init: count: int -> initializer: (int -> 'T) -> taskSeq<'T>
    val initAsync: count: int -> initializer: (int -> Task<'T>) -> taskSeq<'T>
    val initInfinite: initializer: (int -> 'T) -> taskSeq<'T>
    val initInfiniteAsync: initializer: (int -> Task<'T>) -> taskSeq<'T>

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions