Moving NposSolution to frame (#11031)

* Move `sp-npos-elections-solution-type`
to `frame-election-provider-support`
First stab at it, will need to amend some more stuff

* Fixing tests

* Fixing tests

* Fixing cargo.toml for std configuration

* fmt

* Committing suggested changes
renaming, and re exporting macro.

* Removing unneeded imports

* Move `NposSolution` to frame

* Removing `npos_election` dependencies
Implementing _fpes better

* some feedback for moving NPoSSolution to frame

* fmt

* more formatting

* Fixed some imports and fmt

* Fixing docs

Co-authored-by: kianenigma <kian@parity.io>
This commit is contained in:
Georges
2022-03-16 21:27:19 +00:00
committed by GitHub
parent 7a54efd1f2
commit 26a8c7e6b2
19 changed files with 312 additions and 292 deletions
@@ -25,5 +25,5 @@ parity-scale-codec = "3.0.0"
scale-info = "2.0.1"
sp-arithmetic = { version = "5.0.0", path = "../../../primitives/arithmetic" }
# used by generate_solution_type:
sp-npos-elections = { version = "4.0.0-dev", path = "../../../primitives/npos-elections" }
frame-election-provider-support = { version = "4.0.0-dev", path = ".." }
trybuild = "1.0.53"
@@ -20,6 +20,7 @@ rand = { version = "0.8", features = ["std", "small_rng"] }
codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive"] }
scale-info = { version = "2.0.1", default-features = false, features = ["derive"] }
frame-election-provider-solution-type = { version = "4.0.0-dev", path = ".." }
frame-election-provider-support = { version = "4.0.0-dev", path = "../.." }
sp-arithmetic = { version = "5.0.0", path = "../../../../primitives/arithmetic" }
sp-runtime = { version = "6.0.0", path = "../../../../primitives/runtime" }
# used by generate_solution_type:
@@ -51,14 +51,14 @@ fn decode_impl(
quote! {
let #name =
<
_npos::sp_std::prelude::Vec<(_npos::codec::Compact<#voter_type>, _npos::codec::Compact<#target_type>)>
_feps::sp_std::prelude::Vec<(_feps::codec::Compact<#voter_type>, _feps::codec::Compact<#target_type>)>
as
_npos::codec::Decode
_feps::codec::Decode
>::decode(value)?;
let #name = #name
.into_iter()
.map(|(v, t)| (v.0, t.0))
.collect::<_npos::sp_std::prelude::Vec<_>>();
.collect::<_feps::sp_std::prelude::Vec<_>>();
}
};
@@ -73,12 +73,12 @@ fn decode_impl(
quote! {
let #name =
<
_npos::sp_std::prelude::Vec<(
_npos::codec::Compact<#voter_type>,
[(_npos::codec::Compact<#target_type>, _npos::codec::Compact<#weight_type>); #c-1],
_npos::codec::Compact<#target_type>,
_feps::sp_std::prelude::Vec<(
_feps::codec::Compact<#voter_type>,
[(_feps::codec::Compact<#target_type>, _feps::codec::Compact<#weight_type>); #c-1],
_feps::codec::Compact<#target_type>,
)>
as _npos::codec::Decode
as _feps::codec::Decode
>::decode(value)?;
let #name = #name
.into_iter()
@@ -87,7 +87,7 @@ fn decode_impl(
[ #inner_impl ],
t_last.0,
))
.collect::<_npos::sp_std::prelude::Vec<_>>();
.collect::<_feps::sp_std::prelude::Vec<_>>();
}
})
.collect::<TokenStream2>();
@@ -100,8 +100,8 @@ fn decode_impl(
.collect::<TokenStream2>();
quote!(
impl _npos::codec::Decode for #ident {
fn decode<I: _npos::codec::Input>(value: &mut I) -> Result<Self, _npos::codec::Error> {
impl _feps::codec::Decode for #ident {
fn decode<I: _feps::codec::Input>(value: &mut I) -> Result<Self, _feps::codec::Error> {
#decode_impl_single
#decode_impl_rest
@@ -123,10 +123,10 @@ fn encode_impl(ident: &syn::Ident, count: usize) -> TokenStream2 {
let #name = self.#name
.iter()
.map(|(v, t)| (
_npos::codec::Compact(v.clone()),
_npos::codec::Compact(t.clone()),
_feps::codec::Compact(v.clone()),
_feps::codec::Compact(t.clone()),
))
.collect::<_npos::sp_std::prelude::Vec<_>>();
.collect::<_feps::sp_std::prelude::Vec<_>>();
#name.encode_to(&mut r);
}
};
@@ -139,8 +139,8 @@ fn encode_impl(ident: &syn::Ident, count: usize) -> TokenStream2 {
let inners_solution_array = (0..c - 1)
.map(|i| {
quote! {(
_npos::codec::Compact(inner[#i].0.clone()),
_npos::codec::Compact(inner[#i].1.clone()),
_feps::codec::Compact(inner[#i].0.clone()),
_feps::codec::Compact(inner[#i].1.clone()),
),}
})
.collect::<TokenStream2>();
@@ -149,19 +149,19 @@ fn encode_impl(ident: &syn::Ident, count: usize) -> TokenStream2 {
let #name = self.#name
.iter()
.map(|(v, inner, t_last)| (
_npos::codec::Compact(v.clone()),
_feps::codec::Compact(v.clone()),
[ #inners_solution_array ],
_npos::codec::Compact(t_last.clone()),
_feps::codec::Compact(t_last.clone()),
))
.collect::<_npos::sp_std::prelude::Vec<_>>();
.collect::<_feps::sp_std::prelude::Vec<_>>();
#name.encode_to(&mut r);
}
})
.collect::<TokenStream2>();
quote!(
impl _npos::codec::Encode for #ident {
fn encode(&self) -> _npos::sp_std::prelude::Vec<u8> {
impl _feps::codec::Encode for #ident {
fn encode(&self) -> _feps::sp_std::prelude::Vec<u8> {
let mut r = vec![];
#encode_impl_single
#encode_impl_rest
@@ -182,8 +182,8 @@ fn scale_info_impl(
let name = format!("{}", vote_field(1));
quote! {
.field(|f|
f.ty::<_npos::sp_std::prelude::Vec<
(_npos::codec::Compact<#voter_type>, _npos::codec::Compact<#target_type>)
f.ty::<_feps::sp_std::prelude::Vec<
(_feps::codec::Compact<#voter_type>, _feps::codec::Compact<#target_type>)
>>()
.name(#name)
)
@@ -194,10 +194,10 @@ fn scale_info_impl(
let name = format!("{}", vote_field(2));
quote! {
.field(|f|
f.ty::<_npos::sp_std::prelude::Vec<(
_npos::codec::Compact<#voter_type>,
(_npos::codec::Compact<#target_type>, _npos::codec::Compact<#weight_type>),
_npos::codec::Compact<#target_type>
f.ty::<_feps::sp_std::prelude::Vec<(
_feps::codec::Compact<#voter_type>,
(_feps::codec::Compact<#target_type>, _feps::codec::Compact<#weight_type>),
_feps::codec::Compact<#target_type>
)>>()
.name(#name)
)
@@ -209,13 +209,13 @@ fn scale_info_impl(
let name = format!("{}", vote_field(c));
quote! {
.field(|f|
f.ty::<_npos::sp_std::prelude::Vec<(
_npos::codec::Compact<#voter_type>,
f.ty::<_feps::sp_std::prelude::Vec<(
_feps::codec::Compact<#voter_type>,
[
(_npos::codec::Compact<#target_type>, _npos::codec::Compact<#weight_type>);
(_feps::codec::Compact<#target_type>, _feps::codec::Compact<#weight_type>);
#c - 1
],
_npos::codec::Compact<#target_type>
_feps::codec::Compact<#target_type>
)>>()
.name(#name)
)
@@ -224,14 +224,14 @@ fn scale_info_impl(
.collect::<TokenStream2>();
quote!(
impl _npos::scale_info::TypeInfo for #ident {
impl _feps::scale_info::TypeInfo for #ident {
type Identity = Self;
fn type_info() -> _npos::scale_info::Type<_npos::scale_info::form::MetaForm> {
_npos::scale_info::Type::builder()
.path(_npos::scale_info::Path::new(stringify!(#ident), module_path!()))
fn type_info() -> _feps::scale_info::Type<_feps::scale_info::form::MetaForm> {
_feps::scale_info::Type::builder()
.path(_feps::scale_info::Path::new(stringify!(#ident), module_path!()))
.composite(
_npos::scale_info::build::Fields::named()
_feps::scale_info::build::Fields::named()
#scale_info_impl_single
#scale_info_impl_double
#scale_info_impl_rest
@@ -88,7 +88,7 @@ pub(crate) fn syn_err(message: &'static str) -> syn::Error {
/// ```
///
/// The given struct provides function to convert from/to `Assignment` as part of
/// `sp_npos_elections::Solution` trait:
/// `frame_election_provider_support::NposSolution` trait:
///
/// - `fn from_assignment<..>(..)`
/// - `fn into_assignment<..>(..)`
@@ -101,7 +101,7 @@ pub(crate) fn syn_err(message: &'static str) -> syn::Error {
///
/// ```
/// # use frame_election_provider_solution_type::generate_solution_type;
/// # use sp_npos_elections::NposSolution;
/// # use frame_election_provider_support::NposSolution;
/// # use sp_arithmetic::per_things::Perbill;
/// generate_solution_type!(
/// #[compact]
@@ -226,11 +226,11 @@ where
}
fn imports() -> Result<TokenStream2> {
match crate_name("sp-npos-elections") {
Ok(FoundCrate::Itself) => Ok(quote! { use crate as _npos; }),
Ok(FoundCrate::Name(sp_npos_elections)) => {
let ident = syn::Ident::new(&sp_npos_elections, Span::call_site());
Ok(quote!( extern crate #ident as _npos; ))
match crate_name("frame-election-provider-support") {
Ok(FoundCrate::Itself) => Ok(quote! { use crate as _feps; }),
Ok(FoundCrate::Name(frame_election_provider_support)) => {
let ident = syn::Ident::new(&frame_election_provider_support, Span::call_site());
Ok(quote!( extern crate #ident as _feps; ))
},
Err(e) => Err(syn::Error::new(Span::call_site(), e)),
}
@@ -39,7 +39,7 @@ pub(crate) fn generate(def: crate::SolutionDef) -> Result<TokenStream2> {
let name = vote_field(1);
// NOTE: we use the visibility of the struct for the fields as well.. could be made better.
quote!(
#vis #name: _npos::sp_std::prelude::Vec<(#voter_type, #target_type)>,
#vis #name: _feps::sp_std::prelude::Vec<(#voter_type, #target_type)>,
)
};
@@ -48,7 +48,7 @@ pub(crate) fn generate(def: crate::SolutionDef) -> Result<TokenStream2> {
let field_name = vote_field(c);
let array_len = c - 1;
quote!(
#vis #field_name: _npos::sp_std::prelude::Vec<(
#vis #field_name: _feps::sp_std::prelude::Vec<(
#voter_type,
[(#target_type, #weight_type); #array_len],
#target_type
@@ -83,9 +83,9 @@ pub(crate) fn generate(def: crate::SolutionDef) -> Result<TokenStream2> {
Eq,
Clone,
Debug,
_npos::codec::Encode,
_npos::codec::Decode,
_npos::scale_info::TypeInfo,
_feps::codec::Encode,
_feps::codec::Decode,
_feps::scale_info::TypeInfo,
)])
};
@@ -101,8 +101,8 @@ pub(crate) fn generate(def: crate::SolutionDef) -> Result<TokenStream2> {
#derives_and_maybe_compact_encoding
#vis struct #ident { #single #rest }
use _npos::__OrInvalidIndex;
impl _npos::NposSolution for #ident {
use _feps::__OrInvalidIndex;
impl _feps::NposSolution for #ident {
const LIMIT: usize = #count;
type VoterIndex = #voter_type;
type TargetIndex = #target_type;
@@ -114,34 +114,34 @@ pub(crate) fn generate(def: crate::SolutionDef) -> Result<TokenStream2> {
}
fn from_assignment<FV, FT, A>(
assignments: &[_npos::Assignment<A, #weight_type>],
assignments: &[_feps::Assignment<A, #weight_type>],
voter_index: FV,
target_index: FT,
) -> Result<Self, _npos::Error>
) -> Result<Self, _feps::Error>
where
A: _npos::IdentifierT,
A: _feps::IdentifierT,
for<'r> FV: Fn(&'r A) -> Option<Self::VoterIndex>,
for<'r> FT: Fn(&'r A) -> Option<Self::TargetIndex>,
{
let mut #struct_name: #ident = Default::default();
for _npos::Assignment { who, distribution } in assignments {
for _feps::Assignment { who, distribution } in assignments {
match distribution.len() {
0 => continue,
#from_impl
_ => {
return Err(_npos::Error::SolutionTargetOverflow);
return Err(_feps::Error::SolutionTargetOverflow);
}
}
};
Ok(#struct_name)
}
fn into_assignment<A: _npos::IdentifierT>(
fn into_assignment<A: _feps::IdentifierT>(
self,
voter_at: impl Fn(Self::VoterIndex) -> Option<A>,
target_at: impl Fn(Self::TargetIndex) -> Option<A>,
) -> Result<_npos::sp_std::prelude::Vec<_npos::Assignment<A, #weight_type>>, _npos::Error> {
let mut #assignment_name: _npos::sp_std::prelude::Vec<_npos::Assignment<A, #weight_type>> = Default::default();
) -> Result<_feps::sp_std::prelude::Vec<_feps::Assignment<A, #weight_type>>, _feps::Error> {
let mut #assignment_name: _feps::sp_std::prelude::Vec<_feps::Assignment<A, #weight_type>> = Default::default();
#into_impl
Ok(#assignment_name)
}
@@ -158,10 +158,10 @@ pub(crate) fn generate(def: crate::SolutionDef) -> Result<TokenStream2> {
all_edges
}
fn unique_targets(&self) -> _npos::sp_std::prelude::Vec<Self::TargetIndex> {
fn unique_targets(&self) -> _feps::sp_std::prelude::Vec<Self::TargetIndex> {
// NOTE: this implementation returns the targets sorted, but we don't use it yet per
// se, nor is the API enforcing it.
use _npos::sp_std::collections::btree_set::BTreeSet;
use _feps::sp_std::collections::btree_set::BTreeSet;
let mut all_targets: BTreeSet<Self::TargetIndex> = BTreeSet::new();
let mut maybe_insert_target = |t: Self::TargetIndex| {
all_targets.insert(t);
@@ -173,22 +173,22 @@ pub(crate) fn generate(def: crate::SolutionDef) -> Result<TokenStream2> {
}
}
type __IndexAssignment = _npos::IndexAssignment<
<#ident as _npos::NposSolution>::VoterIndex,
<#ident as _npos::NposSolution>::TargetIndex,
<#ident as _npos::NposSolution>::Accuracy,
type __IndexAssignment = _feps::IndexAssignment<
<#ident as _feps::NposSolution>::VoterIndex,
<#ident as _feps::NposSolution>::TargetIndex,
<#ident as _feps::NposSolution>::Accuracy,
>;
impl<'a> _npos::sp_std::convert::TryFrom<&'a [__IndexAssignment]> for #ident {
type Error = _npos::Error;
impl<'a> _feps::sp_std::convert::TryFrom<&'a [__IndexAssignment]> for #ident {
type Error = _feps::Error;
fn try_from(index_assignments: &'a [__IndexAssignment]) -> Result<Self, Self::Error> {
let mut #struct_name = #ident::default();
for _npos::IndexAssignment { who, distribution } in index_assignments {
for _feps::IndexAssignment { who, distribution } in index_assignments {
match distribution.len() {
0 => {}
#from_index_impl
_ => {
return Err(_npos::Error::SolutionTargetOverflow);
return Err(_feps::Error::SolutionTargetOverflow);
}
}
};
@@ -310,7 +310,7 @@ pub(crate) fn into_impl(
let name = vote_field(1);
quote!(
for (voter_index, target_index) in self.#name {
#assignments.push(_npos::Assignment {
#assignments.push(_feps::Assignment {
who: voter_at(voter_index).or_invalid_index()?,
distribution: vec![
(target_at(target_index).or_invalid_index()?, #per_thing::one())
@@ -329,25 +329,25 @@ pub(crate) fn into_impl(
let mut inners_parsed = inners
.iter()
.map(|(ref t_idx, p)| {
sum = _npos::sp_arithmetic::traits::Saturating::saturating_add(sum, *p);
sum = _feps::sp_arithmetic::traits::Saturating::saturating_add(sum, *p);
let target = target_at(*t_idx).or_invalid_index()?;
Ok((target, *p))
})
.collect::<Result<_npos::sp_std::prelude::Vec<(A, #per_thing)>, _npos::Error>>()?;
.collect::<Result<_feps::sp_std::prelude::Vec<(A, #per_thing)>, _feps::Error>>()?;
if sum >= #per_thing::one() {
return Err(_npos::Error::SolutionWeightOverflow);
return Err(_feps::Error::SolutionWeightOverflow);
}
// defensive only. Since Percent doesn't have `Sub`.
let p_last = _npos::sp_arithmetic::traits::Saturating::saturating_sub(
let p_last = _feps::sp_arithmetic::traits::Saturating::saturating_sub(
#per_thing::one(),
sum,
);
inners_parsed.push((target_at(t_last_idx).or_invalid_index()?, p_last));
#assignments.push(_npos::Assignment {
#assignments.push(_feps::Assignment {
who: voter_at(voter_index).or_invalid_index()?,
distribution: inners_parsed,
});
@@ -167,17 +167,86 @@
#![cfg_attr(not(feature = "std"), no_std)]
pub mod onchain;
use frame_support::{traits::Get, BoundedVec};
pub mod traits;
use codec::{Decode, Encode};
use frame_support::{traits::Get, BoundedVec, RuntimeDebug};
use sp_runtime::traits::Bounded;
use sp_std::{fmt::Debug, prelude::*};
/// Re-export some type as they are used in the interface.
/// Re-export the solution generation macro.
pub use frame_election_provider_solution_type::generate_solution_type;
/// Re-export some type as they are used in the interface.
pub use sp_arithmetic::PerThing;
pub use sp_npos_elections::{
Assignment, ElectionResult, ExtendedBalance, IdentifierT, PerThing128, Support, Supports,
VoteWeight,
Assignment, ElectionResult, Error, ExtendedBalance, IdentifierT, PerThing128, Support,
Supports, VoteWeight,
};
pub use traits::NposSolution;
// re-export for the solution macro, with the dependencies of the macro.
#[doc(hidden)]
pub use codec;
#[doc(hidden)]
pub use scale_info;
#[doc(hidden)]
pub use sp_arithmetic;
#[doc(hidden)]
pub use sp_std;
// Simple Extension trait to easily convert `None` from index closures to `Err`.
//
// This is only generated and re-exported for the solution code to use.
#[doc(hidden)]
pub trait __OrInvalidIndex<T> {
fn or_invalid_index(self) -> Result<T, Error>;
}
impl<T> __OrInvalidIndex<T> for Option<T> {
fn or_invalid_index(self) -> Result<T, Error> {
self.ok_or(Error::SolutionInvalidIndex)
}
}
/// The [`IndexAssignment`] type is an intermediate between the assignments list
/// ([`&[Assignment<T>]`][Assignment]) and `SolutionOf<T>`.
///
/// The voter and target identifiers have already been replaced with appropriate indices,
/// making it fast to repeatedly encode into a `SolutionOf<T>`. This property turns out
/// to be important when trimming for solution length.
#[derive(RuntimeDebug, Clone, Default)]
#[cfg_attr(feature = "std", derive(PartialEq, Eq, Encode, Decode))]
pub struct IndexAssignment<VoterIndex, TargetIndex, P: PerThing> {
/// Index of the voter among the voters list.
pub who: VoterIndex,
/// The distribution of the voter's stake among winning targets.
///
/// Targets are identified by their index in the canonical list.
pub distribution: Vec<(TargetIndex, P)>,
}
impl<VoterIndex, TargetIndex, P: PerThing> IndexAssignment<VoterIndex, TargetIndex, P> {
pub fn new<AccountId: IdentifierT>(
assignment: &Assignment<AccountId, P>,
voter_index: impl Fn(&AccountId) -> Option<VoterIndex>,
target_index: impl Fn(&AccountId) -> Option<TargetIndex>,
) -> Result<Self, Error> {
Ok(Self {
who: voter_index(&assignment.who).or_invalid_index()?,
distribution: assignment
.distribution
.iter()
.map(|(target, proportion)| Some((target_index(target)?, proportion.clone())))
.collect::<Option<Vec<_>>>()
.or_invalid_index()?,
})
}
}
/// A type alias for [`IndexAssignment`] made from [`NposSolution`].
pub type IndexAssignmentOf<C> = IndexAssignment<
<C as NposSolution>::VoterIndex,
<C as NposSolution>::TargetIndex,
<C as NposSolution>::Accuracy,
>;
/// Types that are used by the data provider trait.
pub mod data_provider {
@@ -0,0 +1,129 @@
// This file is part of Substrate.
// Copyright (C) 2019-2022 Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// distributed under the License is distributed on an "AS IS" BASIS,
// See the License for the specific language governing permissions and
// limitations under the License.
//! Traits for the election operations.
use crate::{Assignment, IdentifierT, IndexAssignmentOf, PerThing128, VoteWeight};
use codec::Encode;
use scale_info::TypeInfo;
use sp_arithmetic::traits::{Bounded, UniqueSaturatedInto};
use sp_npos_elections::{ElectionScore, Error, EvaluateSupport};
use sp_std::{
convert::{TryFrom, TryInto},
fmt::Debug,
prelude::*,
};
/// An opaque index-based, NPoS solution type.
pub trait NposSolution
where
Self: Sized + for<'a> sp_std::convert::TryFrom<&'a [IndexAssignmentOf<Self>], Error = Error>,
{
/// The maximum number of votes that are allowed.
const LIMIT: usize;
/// The voter type. Needs to be an index (convert to usize).
type VoterIndex: UniqueSaturatedInto<usize>
+ TryInto<usize>
+ TryFrom<usize>
+ Debug
+ Copy
+ Clone
+ Bounded
+ Encode
+ TypeInfo;
/// The target type. Needs to be an index (convert to usize).
type TargetIndex: UniqueSaturatedInto<usize>
+ TryInto<usize>
+ TryFrom<usize>
+ Debug
+ Copy
+ Clone
+ Bounded
+ Encode
+ TypeInfo;
/// The weight/accuracy type of each vote.
type Accuracy: PerThing128;
/// Get the length of all the voters that this type is encoding.
///
/// This is basically the same as the number of assignments, or number of active voters.
fn voter_count(&self) -> usize;
/// Get the total count of edges.
///
/// This is effectively in the range of {[`Self::voter_count`], [`Self::voter_count`] *
/// [`Self::LIMIT`]}.
fn edge_count(&self) -> usize;
/// Get the number of unique targets in the whole struct.
///
/// Once presented with a list of winners, this set and the set of winners must be
/// equal.
fn unique_targets(&self) -> Vec<Self::TargetIndex>;
/// Get the average edge count.
fn average_edge_count(&self) -> usize {
self.edge_count().checked_div(self.voter_count()).unwrap_or(0)
}
/// Compute the score of this solution type.
fn score<A, FS>(
self,
stake_of: FS,
voter_at: impl Fn(Self::VoterIndex) -> Option<A>,
target_at: impl Fn(Self::TargetIndex) -> Option<A>,
) -> Result<ElectionScore, Error>
where
for<'r> FS: Fn(&'r A) -> VoteWeight,
A: IdentifierT,
{
let ratio = self.into_assignment(voter_at, target_at)?;
let staked =
sp_npos_elections::helpers::assignment_ratio_to_staked_normalized(ratio, stake_of)?;
let supports = sp_npos_elections::to_supports(&staked);
Ok(supports.evaluate())
}
/// Remove a certain voter.
///
/// This will only search until the first instance of `to_remove`, and return true. If
/// no instance is found (no-op), then it returns false.
///
/// In other words, if this return true, exactly **one** element must have been removed self.
fn remove_voter(&mut self, to_remove: Self::VoterIndex) -> bool;
/// Build self from a list of assignments.
fn from_assignment<FV, FT, A>(
assignments: &[Assignment<A, Self::Accuracy>],
voter_index: FV,
target_index: FT,
) -> Result<Self, Error>
where
A: IdentifierT,
for<'r> FV: Fn(&'r A) -> Option<Self::VoterIndex>,
for<'r> FT: Fn(&'r A) -> Option<Self::TargetIndex>;
/// Convert self into a `Vec<Assignment<A, Self::Accuracy>>`
fn into_assignment<A: IdentifierT>(
self,
voter_at: impl Fn(Self::VoterIndex) -> Option<A>,
target_at: impl Fn(Self::TargetIndex) -> Option<A>,
) -> Result<Vec<Assignment<A, Self::Accuracy>>, Error>;
}