F #: могу ли я расширить модуль дополнительным модулем, просто сославшись на другую сборку? - PullRequest
1 голос
/ 18 июня 2020

Я работаю над крошечной F # ADO. NET "оболочкой" (да, еще одной, помимо Npg Зайда Аджаджа sql .FSharp , Дональда Пима Брауэрса и многие другие на GitHub), и я думаю о расширении поддержки для различных ADO. NET провайдеров ...

В основном у меня есть основной проект (ie. Michelle.Sql.Core), содержащий основные типы + функции, немного похожи на Dapper:

type IDbValue<'DbConnection, 'DbParameter
    when 'DbConnection :> DbConnection
    and 'DbParameter :> DbParameter> =

    abstract ToParameter: string -> 'DbParameter

type CommandDefinition<'DbConnection, 'DbParameter, 'DbType
    when 'DbConnection :> DbConnection
    and 'DbParameter :> DbParameter
    and 'DbType :> IDbValue<'DbConnection, 'DbParameter>> =
    { Statement: Statement
      Parameters: (string * 'DbType) list
      CancellationToken: CancellationToken
      Timeout: TimeSpan
      StoredProcedure: bool
      Prepare: bool
      Transaction: DbTransaction option }

Во-первых, вы можете подумать: «Wo sh есть много универсальных шаблонов, украшающих ваши определения типов!».

Хорошо, обо всем по порядку. Я пытаюсь обойти некоторые ограничения, наиболее заметные из которых: https://github.com/fsharp/fslang-suggestions/issues/255 (вместе с его хорошим другом ), подумал Я мог бы обойти эту проблему, создав проект C# и наложив ограничения в этом проекте, это не сработает.

Причина, по которой мне нужно много общих c ограничений, заключается в том, что я хочу сильно- типизированное соединение, которое как бы "протекает" через вызовы, устанавливая значения различных полей этой записи, например:

let playWithSQLite() = 
    use connection = new SQLiteConnection()
    Sql.statement "INSERT INTO aTable (aColumn) VALUES(@aNumber);"
    |> Sql.prepare true
    |> Sql.timeout (TimeSpan.FromMinutes(1.))
    |> Sql.parameters [("aNumber", SqliteDbValue.Integer 42L)]
    |> Sql.executeNonQuery connection

Fyi, SqliteDbValue определяется в другой сборке Michelle.Sql.Sqlite:

// https://www.sqlite.org/datatype3.html
type SqliteDbValue =
    | Null
    | Integer of int64
    | Real of double
    | Text of string
    | Blob of byte array
    interface IDbValue<SQLiteConnection, SQLiteParameter> with
        member this.ToParameter(name) =
            let parameter = SQLiteParameter()
            // Not so secret impl. goes here...
            parameter

Приведенный выше код работает, в основном, запись CommandDefinition заполняется различными вызовами, определенными в основной библиотеке через модуль Sql (украшенный RequiredAccessAttribute).

Проблема возникает, когда при использовании необходимо явно указать общий c возвращаемый тип ...

[<RequireQualifiedAccess>]
module Sql = 

    // [...]

    let executeNonQuery
        (connection: 'DbConnection when 'DbConnection :> DbConnection)
        (commandDefinition: CommandDefinition<'DbConnection, 'DbParameter, 'DbType>
            when 'DbConnection :> DbConnection
            and 'DbParameter :> DbParameter
            and 'DbType :> IDbValue<'DbConnection, 'DbParameter>) =
        async { 
            // Not so secret impl. goes here
        }

    let executeScalar<'Scalar, .. >
        (connection: 'DbConnection when 'DbConnection :> DbConnection)
        (commandDefinition: CommandDefinition<'DbConnection, 'DbParameter, 'DbType>
            when 'DbConnection :> DbConnection
            and 'DbParameter :> DbParameter
            and 'DbType :> IDbValue<'DbConnection, 'DbParameter>) =
        async { 
            // Not so secret impl. goes here
        }

Итак, вы видите, что в случае функции executeScalar выше, поскольку один тип должен быть явным, это означает, что каждый другой параметр generi c теперь должен быть явным при вызове этой функции, в противном случае они имеют значение по умолчанию obj, что, среди прочего, означает, что конечный пользователь теперь должен ввести 4 общих c параметра:

// [...] setting up the CommandDefinition...
|> Sql.executeScalar<int64, SQLiteConnection, SQLiteParameter, SqliteDbValue> connection

, и это именно то, чего я хотел бы избежать при сохранении согласованности соединения.

Что Я пробовал, и это довольно неуклюжее решение - реализовать сокращенную версию executeScalar, и что я имею в виду под:

module Michelle.Sql.Sqlite


[<RequireQualifiedAccess>]
module Sql =
    let executeScalar<'Scalar> connection commandDefinition =
        Sql.executeScalar<'Scalar, SQLiteConnection, SQLiteParameter, SqliteDbValue>
            connection
            commandDefinition

Но суть стратегии в том, что она, по сути, сводится к затенению:

Следовательно, этот код ниже не работает:

open Michelle.Sql.Sqlite
open Michelle.Sql.Core

// [...] setting up the CommandDefinition... connection being an instance of SQLiteConnection
|> Sql.executeScalar<int64> connection

Пока он работает:

open Michelle.Sql.Core
open Michelle.Sql.Sqlite

// [...] setting up the CommandDefinition... connection being an instance of SQLiteConnection
|> Sql.executeScalar<int64> connection

I wi sh может быть решение, я хотя про stati c classe s, но частичные классы не могут быть определены в нескольких сборках.

Я знаю, что перегрузка невозможна с функциями модуля F #, и затенение не похоже на жизнеспособное решение с точки зрения опыта разработчика.

Итак, есть ли какое-нибудь решение? (Не говоря уже о создании другой функции с другим именем или другого модуля с другим именем)

1 Ответ

0 голосов
/ 20 июня 2020

Кениг Лир предложил:

Не совсем ответ, но почему бы вам не переименовать исходный модуль в Core SQL, тогда вы можете создавать модули для каждого типа драйвера, например Sql.execluteScalar<'T> = CoreSql.executeScalar<'T,SQLLiteConnection, etc. и предоставить псевдоним для каждой отдельной функции и никогда не раскрывать Core SQL.

, и это в значительной степени то, что я в конечном итоге сделал:


// open stuff goes here...

type SqliteCommandDefinition = CommandDefinition<SQLiteConnection, SQLiteParameter, SqliteDbValue>

[<RequireQualifiedAccess>]
module Sqlite =

    // Other functions irrelevant to this post

    let executeScalar<'Scalar> connection (commandDefinition: SqliteCommandDefinition) =
        Sql.executeScalar<'Scalar, _, _, _>
            connection
            commandDefinition

    let executeNonQuery connection (commandDefinition: SqliteCommandDefinition) =
        Sql.executeNonQuery connection commandDefinition

...