Структура 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.
Практика для закрепления:
- переставьте
Domain.fsиProgram.fsместами в учебном проекте; - запустите сборку и посмотрите ошибку;
- верните правильный порядок и пересоберите.
Так правило порядка файлов запоминается на реальном эффекте, а не как абстрактное ограничение.
Словарь файлов проекта
| Файл | Назначение |
|---|---|
.fsproj | Манифест проекта: целевой framework, список .fs, ссылки на пакеты и проекты |
.fs | Исходник F# |
.fsi | Файл подписи (интерфейс): видимые снаружи сигнатуры без реализации |
Program.fs | Обычно точка входа консольного приложения |
.sln | Solution — несколько проектов (библиотека + приложение + тесты) |
Минимальный консольный проект
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 опирается на компилятор) |
Тест не видит internal | InternalsVisibleTo в .fsproj библиотеки |
Сборка и проверка
dotnet build
dotnet test
dotnet run --project src/DemoApp/DemoApp.fsproj
Предупреждения компилятора (--warnon) задают в <OtherFlags> — см. Справочник по F#.
Маршрут обучения F# в разделе
- F# в экосистеме .NET
- Первая программа
- Интерактивная работа (FSI)
- Сопоставление с образцом
- Императивные конструкции · ООП для interop · Асинхронность
- Структура проекта (эта статья)
- Справочник по F# · Справочник Learn
Базовый разбор HTTP и HTTPS находится в отдельной статье — HTTP как основа веб-интеграций.