Перейти к основному содержимому

Структура F#-проекта

Разработчику Архитектору

Чем F#-проект отличается от C#

В C# порядок файлов в .csproj почти не важен: класс из B.cs видит класс из A.cs в том же проекте.

В F# компилятор обрабатывает файлы строго в порядке, указанном в .fsproj (или в порядке по умолчанию SDK, пока вы не добавили свои файлы). Код в файле может использовать только то, что объявлено:

  • в предыдущих файлах проекта, или
  • выше по тексту в том же файле.

Типичная ошибка:

error FS0039: The value, namespace, type or module 'Order' is not defined.

Частая причина — Program.fs стоит выше Domain.fs, хотя Program ссылается на тип Order из Domain.


Словарь файлов проекта

ФайлНазначение
.fsprojМанифест проекта: целевой framework, список .fs, ссылки на пакеты и проекты
.fsИсходник F#
.fsiФайл подписи (интерфейс): видимые снаружи сигнатуры без реализации
Program.fsОбычно точка входа консольного приложения
.slnSolution — несколько проектов (библиотека + приложение + тесты)

Минимальный консольный проект

dotnet new console -lang F# -n DemoApp
cd DemoApp
dotnet run

Структура:

DemoApp/
DemoApp.fsproj
Program.fs

В простом шаблоне один файл — порядок очевиден. Как только появляется второй .fs, порядок нужно контролировать явно.


Явный порядок в .fsproj

<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Compile Include="Domain.fs" />
<Compile Include="Services.fs" />
<Compile Include="Program.fs" />
</ItemGroup>
</Project>

Рекомендуемое содержимое слоёв:

ФайлЧто держать
Domain.fsтипы, DU, записи, чистые функции без I/O
Services.fsоркестрация, HTTP, БД, файлы
Program.fs[<EntryPoint>], меню, композиция, dotnet run

Правило: от данных к эффектам, от общего к частному. Так зависимости читаются сверху вниз в .fsproj.

Пример Domain.fs:

module DemoApp.Domain

type OrderId = OrderId of int

type Order = { Id: OrderId; Amount: decimal }

Services.fs:

module DemoApp.Services

open DemoApp.Domain

let describe (Order { Id = OrderId n; Amount = a }) =
$"Order {n}, amount {a}"

Program.fs вызывает Services.describe — оба модуля уже скомпилированы выше.


Библиотека и ссылка из приложения

dotnet new classlib -lang F# -n DemoLib
dotnet new console -lang F# -n DemoApp
dotnet add DemoApp/DemoApp.fsproj reference DemoLib/DemoLib.fsproj

В DemoLib.fsproj:

<ItemGroup>
<Compile Include="Types.fs" />
<Compile Include="Api.fs" />
</ItemGroup>

В приложении:

open DemoLib

Имя модуля по умолчанию совпадает с именем файла (Types, Api) или задаётся через module MyName = в начале файла. Для C#-клиентов: [<CLIMutable>] на DTO, осмысленные имена, при конфликтах — [<RequireQualifiedAccess>] на модуле.


Модули, namespace и один файл

namespace MyCompany.Demo

module Domain =
type Id = Id of int

module Services =
open Domain
let format (Id n) = $"#{n}"
  • namespace — префикс имён для CLR.
  • module — группа функций и типов; полное имя может быть MyCompany.Demo.Domain.
  • В файле с namespace весь код верхнего уровня должен быть внутри module или type.

Несколько module в одном файле компилируются сверху вниз; при сложных зависимостях проще разнести модули по разным .fs.


Файлы подписей .fsi

Пара:

Domain.fsi — только сигнатуры (контракт)
Domain.fs — реализация

В .fsproj .fsi строго перед .fs:

<Compile Include="Domain.fsi" />
<Compile Include="Domain.fs" />

Клиент библиотеки видит только то, что в .fsi; детали реализации скрыты. Полезно для публичных NuGet-пакетов на F#.


Solution из нескольких проектов

src/
App.Core/ # F# — домен
App.Infrastructure/ # F# или C# — БД, файлы
App.Web/ # ASP.NET Core (часто C#)
tests/
App.Core.Tests/

У каждого .fsproj свой порядок <Compile Include="..."/>. Смешанный solution (C# UI + F# core) распространён: F# собирается в DLL, C# добавляет <ProjectReference> — без специальных флагов, кроме продуманного публичного API.


Связь с FSI и #load

Скрипт:

#load "Domain.fs"
#load "Services.fs"

Порядок #load должен повторять порядок в .fsproj. Иначе в REPL та же FS0039, что и при сборке.

См. Интерактивная работа (FSI).


Частые проблемы

СимптомЧто проверить
FS0039 — имя не найденоПоднять файл с определением выше в .fsproj
Циклическая зависимость двух файловВынести общие типы в третий файл или объединить модули
Два модуля с одним именемРазные namespace или RequireQualifiedAccess
IntelliSense пустойУспешный dotnet build (Ionide опирается на компилятор)
Тест не видит internalInternalsVisibleTo в .fsproj библиотеки

Сборка и проверка

dotnet build
dotnet test
dotnet run --project src/DemoApp/DemoApp.fsproj

Предупреждения компилятора (--warnon) задают в <OtherFlags> — см. Справочник по F#.


Маршрут обучения F# в разделе

  1. F# в экосистеме .NET
  2. Первая программа
  3. Интерактивная работа (FSI)
  4. Сопоставление с образцом
  5. Императивные конструкции · ООП для interop · Асинхронность
  6. Структура проекта (эта статья)
  7. Справочник по F# · Справочник Learn

См. также

Другие статьи этого же раздела в боковом меню (как на странице «О разделе»).